docs(spikes): OpenWorkflow and Pi SDK compatibility spikes + ADR-002/003 (W0-B)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
8b90d396b2
commit
ec023a8405
16 files changed
+2884
No files matched your search
@@ -0,0 +1,178 @@
|
||||
# W0-B spike:OpenWorkflow 0.10.1 兼容性验证记录
|
||||
|
||||
- 日期:2026-09-27;环境:Node v24.15.0、PostgreSQL 16(`postgres:16-alpine`,容器 `umt-spike-pg`,端口 55434)、`[email protected]`、`[email protected]`、`[email protected]` + `pg@8`
|
||||
- 代码:`spikes/openworkflow/`(`common.ts` 定义工作流,`worker.ts` 起 worker,`cli.ts` 投递/信号/取消/查询)
|
||||
- 业务副作用账本:每个步骤体执行时往 `biz.side_effect_ledger`(Kysely + pg)插一行,用来判断步骤有没有重跑、有没有在失去租约后继续写。
|
||||
- 结论见 `docs/adr/ADR-002-task-engine.md`。
|
||||
|
||||
## 0. API 形状(以 node_modules 内 .d.ts 为准)
|
||||
|
||||
```ts
|
||||
import { OpenWorkflow } from "openworkflow";
|
||||
import { BackendPostgres } from "openworkflow/postgres";
|
||||
|
||||
const backend = await BackendPostgres.connect(url, { schema: "openworkflow", namespaceId?, runMigrations?: true });
|
||||
// 或 BackendPostgres.fromPool(postgresJsSql, { schema }) —— 共享 postgres.js 连接池,不跑迁移
|
||||
const ow = new OpenWorkflow({ backend });
|
||||
const wf = ow.defineWorkflow({ name, version?, schema?: StandardSchema, retryPolicy?: { maximumAttempts, initialInterval, backoffCoefficient, maximumInterval } },
|
||||
async ({ input, step, run, version }) => {
|
||||
await step.run({ name, retryPolicy? }, async () => ...); // 结果持久化,重放时直接返回
|
||||
await step.sleep(name, "3s" | "7 days"); // 持久定时
|
||||
const s = await step.waitForSignal<T>({ name?, signal, timeout?, schema? }); // 超时返回 null,默认超时 1 年
|
||||
await step.sendSignal({ signal, data }); await step.runWorkflow(spec, input, { timeout });
|
||||
});
|
||||
const h = await wf.run(input, { idempotencyKey?, availableAt?, deadlineAt? }); // h.workflowRun.id / h.result() / h.cancel()
|
||||
await ow.sendSignal({ signal, data, idempotencyKey? }); // → { workflowRunIds }
|
||||
await ow.cancelWorkflowRun(id);
|
||||
const worker = ow.newWorker({ concurrency }); await worker.start(); await worker.stop();
|
||||
backend.getWorkflowRun({ workflowRunId }); backend.listStepAttempts({ workflowRunId }); backend.listWorkflowRuns({ status, workflowName });
|
||||
```
|
||||
|
||||
- 运行状态:`pending | running | sleeping(legacy) | completed | succeeded | failed | canceled`。等待信号 / sleep 期间,run 的 status 仍是 `running`,只是 `worker_id` 为空且 `available_at` 指向唤醒时间。
|
||||
- 默认值(源码):工作流 `maximumAttempts: 1`;步骤 `maximumAttempts: 10`、`1s` 起指数退避、上限 `100s`;租约 `30s`,由心跳每 15s 续期(不可配置);单个 run 最多 1000 个 step attempt。
|
||||
- 版本:注册表的键是 `(name, version)`;run 创建时记下 version,只有注册了同一 version 的 worker 才会执行它。
|
||||
|
||||
## 验证结果
|
||||
|
||||
### (a) 表初始化 / 迁移 —— 通过
|
||||
|
||||
`BackendPostgres.connect()` 默认执行幂等迁移(`CREATE SCHEMA IF NOT EXISTS` + `openworkflow_migrations` 版本表)。另有 `dist/postgres/scripts/db-migrate.js`(只认默认 URL,生产不宜直接用)。`fromPool()` 不跑迁移。
|
||||
|
||||
```
|
||||
$ npx tsx openworkflow/cli.ts tables
|
||||
biz.side_effect_ledger
|
||||
openworkflow.openworkflow_migrations
|
||||
openworkflow.step_attempts
|
||||
openworkflow.workflow_runs
|
||||
openworkflow.workflow_signals
|
||||
```
|
||||
|
||||
建议:在 `packages/db` 的迁移流程里调用 `migrate(sql, "openworkflow")`(`openworkflow/internal` 导出),应用启动时用 `runMigrations:false`。**未验证** `openworkflow/internal` 的 `migrate` 直接调用。
|
||||
|
||||
### 主路径 —— 通过
|
||||
|
||||
```
|
||||
{"runId":"bdc8f92d-…","status":"pending","version":"v1"}
|
||||
{"status":"running",…} ← 停在 wait-download
|
||||
{"workflowRunIds":["bdc8f92d-…"]} ← signal download-done:d1
|
||||
{"status":"completed","attempts":3,"output":{"sub":{"torrentHash":"hash-d1"},"copied":{"dest":"/lib/d1.mkv"},"signal":{"ok":true}}}
|
||||
submit-download function completed / wait-download signal-wait completed / copy-file function completed
|
||||
register function completed / retention sleep completed / cleanup function completed
|
||||
```
|
||||
|
||||
### (b) kill -9 后重启恢复 —— 通过(语义:已完成步骤不重跑,正在执行的步骤至少执行一次)
|
||||
|
||||
copy-file 步骤内等待 10s;在复制途中对 worker `kill -9`,再启动新 worker A2:
|
||||
|
||||
```
|
||||
kill 之后: {"status":"running","attempts":2} copy-file function running;available_at 比 now 晚约 16s(租约尚未到期)
|
||||
A2 启动约 30s 后:{"status":"completed","attempts":4}
|
||||
submit-download completed / wait-download completed / copy-file running(孤儿行) / copy-file completed / register / retention / cleanup
|
||||
账本:
|
||||
9e50537b submit-download A version=v1
|
||||
9e50537b copy-file A start ← 被 kill 的那次
|
||||
9e50537b copy-file A2 start ← 同一步骤重跑
|
||||
9e50537b copy-file A2 end
|
||||
9e50537b register A2 / cleanup A2
|
||||
```
|
||||
|
||||
- submit-download 与信号等待没有重跑;copy-file 在新 worker 上从头执行。所以步骤体必须幂等(例如复制前先查目标文件和操作日志)。
|
||||
- 要等租约到期(最长约 30s)才会被重新认领。
|
||||
- 被中断的 attempt 行一直停在 `running`,不会被清掉。展示时不要按这张表统计"执行中"。
|
||||
|
||||
### 租约丢失后的写入隔离(补充:SIGSTOP 模拟 worker 卡死) —— 引擎层通过,业务层没有隔离
|
||||
|
||||
对 A 执行 `kill -STOP`,由 B 接管并完成,再 `kill -CONT` A:
|
||||
|
||||
```
|
||||
d23c7db1 copy-file A start
|
||||
d23c7db1 copy-file B start / B end / register B / cleanup B ← run completed
|
||||
d23c7db1 copy-file A end ← A 恢复后步骤体照常跑完并写了业务账本
|
||||
Heartbeat failed: Error: Failed to extend lease for workflow run ← A 的步骤结果被引擎拒绝(worker_id 不再匹配)
|
||||
```
|
||||
|
||||
引擎保证过期 worker 的**步骤结果**不会写入;但步骤体内的**业务副作用**照常发生,而且步骤体拿不到 AbortSignal 或租约状态。REQ §11.1 要求"失去租约的 worker 不得再提交业务状态",这一条要靠业务侧的操作账本做 fencing(见 ADR-002)。
|
||||
|
||||
### (c) 信号先于等待到达 —— **会丢**
|
||||
|
||||
```
|
||||
$ cli signal d2 early-key → {"workflowRunIds":[]} ← 还没有等待者,直接丢弃
|
||||
$ cli start-acquire d2 → run 停在 wait-download(running)
|
||||
$ cli signal d2 early-key → {"workflowRunIds":["f837c0cb-…"]} ← 同一个 idempotencyKey 仍能投递(前一次没落库)
|
||||
$ cli signal d2 key2 → {"workflowRunIds":[]} ← 等待已结束,再发就丢
|
||||
```
|
||||
|
||||
源码 `sendSignal` 只会 `SELECT … step_attempts WHERE kind='signal-wait' AND status='running' FOR UPDATE`,没有等待者就返回空数组,不落库(类型注释也写了:"If no workflow is waiting, the signal is silently dropped")。同一信号名同一时间只允许一个等待步骤。**不能把 signal 当持久授权或完成通知。**
|
||||
|
||||
### (d) 取消传播 —— 部分通过
|
||||
|
||||
```
|
||||
cancel slow(处于 long 步骤中) → {"status":"canceled"}
|
||||
账本:long start … long end (still ran after cancel?) ← 进行中的步骤体跑完了(没有 AbortSignal)
|
||||
after-long 未执行;long 的 attempt 行停在 running
|
||||
cancel acquire(处于 wait-download) → {"status":"canceled"},之后的步骤不再执行
|
||||
但随后 signal d4 → {"workflowRunIds":["282dde2b-…"]} ← 对已取消的 run 仍然"投递成功"
|
||||
cancel 已完成 run → Error: Cannot cancel workflow run … with status completed
|
||||
```
|
||||
|
||||
取消只能保证"后续步骤不再执行"。进行中的步骤不会被中断,子工作流是否级联取消**未验证**。
|
||||
|
||||
### (e) 步骤失败重试与最大次数 —— 通过
|
||||
|
||||
```
|
||||
flaky(前 2 次失败, maximumAttempts=3):completed, attempts=3
|
||||
flaky-step failed "boom attempt 1" / failed "boom attempt 2" / completed
|
||||
flaky(前 5 次失败, maximumAttempts=3):failed, attempts=3, error.message="boom attempt 3"
|
||||
flaky-step failed ×3,不再重试
|
||||
```
|
||||
|
||||
步骤重试的实现方式是整个 run 按退避重新排期、重放前面的步骤;run 的 `attempts` 会随之增长。重试期间 run 显示为 `pending`,错误写在 run.error 上。
|
||||
|
||||
### (f) idempotencyKey 去重 —— 通过(有限制)
|
||||
|
||||
```
|
||||
start-acquire d5 same-key → runId fd705d54… pending
|
||||
start-acquire d5 same-key → runId fd705d54… running ← 返回已有 run
|
||||
start-acquire d5-other same-key → runId fd705d54… ← 输入不同也返回旧 run,不报冲突
|
||||
select count(*) … where idempotency_key='same-key' → 1
|
||||
```
|
||||
|
||||
去重范围是 `(namespace, workflowName, key)`,用 advisory lock 串行化,**窗口只有 24 小时**(`DEFAULT_RUN_IDEMPOTENCY_PERIOD_MS`)。引擎不比较输入。
|
||||
|
||||
### (g) 版本升级 —— 通过(要自己管好版本纪律)
|
||||
|
||||
1. run 以 `version=v1` 停在等待中;停掉 v1 worker,只起 `WF_VERSION=v2` 的 worker,然后发信号:
|
||||
```
|
||||
{"status":"pending","attempts":2,"error":{"message":"Workflow \"acquire\" (version: v1) is not registered"}}
|
||||
```
|
||||
run 没有失败,而是按退避无限重排(`MISSING_DEFINITION_RETRY_POLICY maximumAttempts:0`),等能处理 v1 的 worker 出现。
|
||||
2. 再起一个 v1 worker,但代码改过(同一 version 在 copy 与 register 之间插入 `v2-extra-verify` 步骤):
|
||||
```
|
||||
completed;steps: submit-download / wait-download / copy-file / v2-extra-verify / register / retention / cleanup
|
||||
```
|
||||
重放按**步骤名**匹配历史,没有确定性检查。新插入的步骤会执行;已完成的步骤只要改了名,就会被当作新步骤重跑。
|
||||
|
||||
### (h) 与 Kysely + pg 共库不同 schema —— 通过
|
||||
|
||||
OpenWorkflow(postgres.js 连接池,schema `openworkflow`)和 Kysely(pg 连接池,schema `biz`)在同一个库里跑完了以上全部用例,没有冲突。限制:OpenWorkflow 自己管连接(postgres.js),`createWorkflowRun` 不能接收 Kysely 事务,所以**不能和业务写入在同一个事务里入队**,要用 outbox。
|
||||
|
||||
### (i) 两个 worker 进程并发 —— 通过
|
||||
|
||||
```
|
||||
worker A、B 同时运行,投递 60 个 burst run
|
||||
{"total":"60","runs":"60","a":"32","b":"28"} ← 60 次执行、60 个 run,没有重复
|
||||
completed|60
|
||||
```
|
||||
|
||||
认领 SQL 用的是 `FOR UPDATE SKIP LOCKED`。
|
||||
|
||||
### 补充:PostgreSQL 重启 —— 通过
|
||||
|
||||
run 处于 15s 的 retention sleep 时执行 `docker restart umt-spike-pg`,约 30s 后 run 已 `completed`,6 个步骤都 completed;worker 日志里有 2 条 tick 失败,之后自动恢复。
|
||||
|
||||
## 未验证
|
||||
|
||||
- Deno 下运行(ADR-001 已选 Node)。
|
||||
- `step.runWorkflow` 子工作流取消级联、`deadlineAt`、OpenTelemetry。
|
||||
- 大量 sleeping run(>10k)下的轮询开销。
|
||||
- "qB 接收后断连""复制完成但结果未提交"这类业务故障注入。它们属于业务幂等层,在 `packages/tasks` 集成测试中覆盖(见 ADR-002)。
|
||||
@@ -0,0 +1,159 @@
|
||||
# W0-B spike:Pi SDK(pi-agent-core / pi-ai 0.87.1)验证记录
|
||||
|
||||
- 日期:2026-09-27;Node v24.15.0;`@earendil-works/[email protected]`、`@earendil-works/[email protected]`(依赖 `@earendil-works/chord`、`pi-telemetry`、`[email protected]`)
|
||||
- 代码:`spikes/pi/`(`common.ts` 工具与脚本化模型、`fake-run.ts`、`abort-resume.ts`、`compaction.ts`、`provider-config.ts`)
|
||||
- 参考:包内 README / .d.ts;GitHub `earendil-works/pi` tag `v0.87.1` 的 `packages/agent/docs/harness.md`(AgentHarness 规范)、`packages/agent/test/harness/runtime/drive-public.test.ts`、`packages/coding-agent/examples/sdk/12-full-control.ts`
|
||||
- 结论见 `docs/adr/ADR-003-pi-integration.md`
|
||||
|
||||
## 0. 该用哪一层
|
||||
|
||||
| 层 | 包 | 说明 | 结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Agent` / `agentLoop` | pi-agent-core | 进程内状态,无持久化 | 不用 |
|
||||
| **`AgentHarness` + `AgentLane`** | pi-agent-core | 持久会话(entry 树 + 操作状态机),有 prompt/resume/abort/compact、事件、hooks、工具重放策略;**不做任何隐式发现** | **采用** |
|
||||
| `createAgentSession` / `AgentSession` | pi-coding-agent | 编码助手层:ResourceLoader 会发现扩展、skills、AGENTS.md,内置 read/bash 等工具,依赖 pi-tui、jiti(运行时加载 TS 扩展)、photon | 不用。REQ §10.1 说的"AgentSession 层"在 0.87.1 里的对应物,就是 core 的 AgentHarness |
|
||||
|
||||
`grep` 过 pi-agent-core dist:只有 `harness/skills.js`(显式调用的 loader)、`env/nodejs.js`(显式构造的执行环境)、`pico3/jsonl.js` 会接触文件系统。`AgentHarness.create()` 只使用传入的 `tools` / `resources` / `systemPrompt`,**不读取 cwd、home、`.pi/`、AGENTS.md**。
|
||||
|
||||
## 1. 关键 API 形状
|
||||
|
||||
```ts
|
||||
import { createModels, createProvider, fauxProvider, fauxAssistantMessage, fauxToolCall, fauxText } from "@earendil-works/pi-ai";
|
||||
import { AgentHarness, BACKGROUND_CONTEXT, MemorySessionRepo, JsonlSessionRepo, type AgentHarnessTool } from "@earendil-works/pi-agent-core";
|
||||
|
||||
const models = createModels(); models.setProvider(provider);
|
||||
const session = await new MemorySessionRepo().create({ id }, ctx); // 或 JsonlSessionRepo({ fileSystem, sessionsRoot })
|
||||
const { harness, open } = await AgentHarness.create({
|
||||
session, models, model,
|
||||
systemPrompt, tools: [/* AgentHarnessTool */], toolContext: { actorId, grants }, // toolContext 可为 (ctx)=>T
|
||||
activeToolNames?, thinkingLevel: "off"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max",
|
||||
compaction: { enabled, reserveTokens, keepRecentTokens },
|
||||
retry: { enabled, maxRetries, baseDelayMs, maxAgentDelayMs? },
|
||||
streamOptions: { timeoutMs, maxRetries, headers, metadata, cacheRetention, deferred },
|
||||
toolExecution: "sequential" | "parallel", resources?: { skills, promptTemplates },
|
||||
}, ctx); // open = 附着时发现的未完成操作
|
||||
const lane = await harness.lane("main", ctx);
|
||||
await lane.prompt(text, undefined, ctx) / lane.resume(ctx) / lane.abort(ctx) / lane.compact({ customInstructions }, ctx)
|
||||
lane.accept({kind:"prompt",operationId,prompt}) + lane.drive({operationId}) + lane.requestAbort(operationId) // 低层原语,可由外部调度器驱动
|
||||
harness.events.on("tool_start" | "tool_end" | "message_update" | "run_end" | "usage" | "compaction_end" | …, listener)
|
||||
harness.hooks.on("before_tool" | "after_tool" | "before_request" | "before_payload" | "transform_context" | "before_compaction" | …, handler)
|
||||
lane.setModel({provider, modelId}) / setThinkingLevel / setActiveTools;harness.setTools / setCompactionSettings / setRetryPolicy
|
||||
```
|
||||
|
||||
工具:
|
||||
|
||||
```ts
|
||||
const t: AgentHarnessTool<Ctx> = {
|
||||
name, label, description, parameters: Type.Object({...}), // typebox 1.x
|
||||
replay: "safe" | "never", // 崩溃后:safe = 用持久化的参数重跑;never = 合成"执行被中断、结果未知"的错误结果
|
||||
executionMode?: "sequential",
|
||||
async execute(toolCallId, params, onUpdate, toolContext, invocation /* invocationId, getMemo/setMemo */, context /* context.abortSignal */) {
|
||||
return { content: [{ type: "text", text }], details }; // 失败直接 throw → isError:true
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 2. 验证结果
|
||||
|
||||
### 2.1 fake 模型驱动两个工具 + 流式事件 —— 通过(`npx tsx pi/fake-run.ts`)
|
||||
|
||||
fake 模型用 `fauxProvider` + `FauxResponseFactory`(函数收到 transcript,按上一个工具结果决定下一步)实现,不需要 API Key。
|
||||
|
||||
```
|
||||
[run_start] lane=main
|
||||
[turn_start] lane=main
|
||||
[text_delta] "我先查一下库里有没有《D"
|
||||
[text_delta] "une》。"
|
||||
[toolcall_end] inventory__query
|
||||
[usage] in=145 totals.input=145 output=18
|
||||
[hook before_tool] inventory__query args={"title":"Dune","year":2021}
|
||||
[tool_start] inventory__query {"title":"Dune","year":2021}
|
||||
[tool_update] lane=main
|
||||
[tool_end] inventory__query isError=false -> "{\"title\":\"Dune\",\"inLibrary\":false,\"versions\":[]}"
|
||||
[turn_end] lane=main
|
||||
[turn_start] lane=main
|
||||
[text_delta] "库里没有,提交 1080" …
|
||||
[toolcall_end] download__submit
|
||||
[toolcall_end] bash
|
||||
[hook before_tool] download__submit args={"title":"Dune","quality":"1080p"}
|
||||
[tool_start] download__submit {"title":"Dune","quality":"1080p"}
|
||||
[tool_end] download__submit isError=false -> "{\"accepted\":true,\"idempotencyKey\":\"dl:Dune:1080p\",\"status\":\"submitted\"}"
|
||||
[tool_start] bash {"command":"rm -rf /"}
|
||||
[tool_end] bash isError=true -> "Tool \"bash\" is unavailable" ← 不在白名单的工具被合成错误结果,不执行
|
||||
[turn_start] … [text_delta] "下载已提交(仅代表已提交,不代表已入库)…"
|
||||
[run_end] status=completed
|
||||
RESULT {"ok":true,"value":{"operationId":"01a0e235-…","kind":"run","status":"completed",…}}
|
||||
SUBMITTED [{"key":"dl:Dune:1080p","title":"Dune"}] fake calls: 3
|
||||
```
|
||||
|
||||
### 2.2 AbortSignal 取消 —— 通过(`SUBMIT_DELAY_MS=5000 npx tsx pi/abort-resume.ts abort`)
|
||||
|
||||
```
|
||||
[tool_start] download__submit
|
||||
>> lane.abort()
|
||||
[operation_abort]
|
||||
[tool_end] download__submit "aborted by signal" ← execute 里的 context.abortSignal 触发了
|
||||
[tool_end] bash "Tool execution was cancelled before completion." ← 同批次尚未执行的调用被合成为取消结果
|
||||
[run_end] aborted
|
||||
PROMPT RESULT {…"status":"aborted"…}
|
||||
```
|
||||
|
||||
### 2.3 会话持久化与崩溃恢复 —— 通过(JSONL)
|
||||
|
||||
进程 1(`crash`)在 `download.submit` 执行中 `SIGKILL` 自己;进程 2(`resume`)打开同一个 JSONL 会话:
|
||||
|
||||
```
|
||||
found session crash-demo …/.sessions/--opt-uhd_media_tool-spikes-pi-.sessions--/2026-09-27T09-33-19-828Z_crash-demo.jsonl
|
||||
open operations at attach: [{"lane":"main","operationId":"01a0e236-3a62-…","kind":"run","startedAt":1790501599842}]
|
||||
[tool_end] (recovery) download__submit "[Tool execution was interrupted. … the external outcome is unknown.]" ← replay:"never" 不重跑
|
||||
[tool_start] (recovery) bash / [tool_end] (recovery) bash "Tool \"bash\" is unavailable"
|
||||
[run_end] completed
|
||||
fake calls: 1 ← 只多请求一次模型;inventory.query 没有重跑
|
||||
```
|
||||
|
||||
恢复时 `download.submit` 得到的是"结果未知"。业务侧要按 `invocationId` 或服务端幂等键去操作账本对账,不能让模型重新提交一次。
|
||||
|
||||
### 2.4 压缩 —— 手动通过,阈值触发可见(`compaction.ts`)
|
||||
|
||||
```
|
||||
[compaction_start] reason=manual / [compaction_end] reason=manual status=completed
|
||||
REPEAT=400:[compaction_start] reason=threshold → [compaction_end] status=completed
|
||||
单条输入本身超过 contextWindow 时 run 失败:{"code":"assistant_error","message":"Assistant request exceeded the context window"}
|
||||
entry types: compaction,message,…
|
||||
```
|
||||
|
||||
摘要本身也是一次模型调用(fake 模型按脚本返回)。压缩提示词可以通过 `customInstructions` 或 `before_compaction` hook 定制。
|
||||
|
||||
### 2.5 真实 provider 类型的配置(不出外网)—— 通过(`provider-config.ts`)
|
||||
|
||||
用 `createProvider` 分别套 `anthropic-messages` 和 `openai-completions` 两种 API 实现,baseUrl 指向脚本自带的本地抓包服务器(返回 400):
|
||||
|
||||
```
|
||||
builtin anthropic models (first 5): claude-fable-5, claude-fable-5-1, claude-haiku-4-5, …
|
||||
builtin anthropic auth configured without env? undefined
|
||||
supported thinking levels, custom anthropic model: off,minimal,low,medium,high
|
||||
[before_request] upstream-anthropic/claude-sonnet-4-5 step=assistant attempt=1
|
||||
[upstream-anthropic] run: {"status":"failed","error":{"code":"assistant_error","message":"400 {…capture server: refusing on purpose…}"}}
|
||||
CAPTURED POST /v1/messages?beta=true auth=sk-test-anthropic
|
||||
model=claude-sonnet-4-5 max_tokens=8192 thinking={"type":"enabled","budget_tokens":7168,"display":"summarized"} tools=["inventory__query"] stream=true
|
||||
CAPTURED POST /v1/chat/completions auth=Bearer sk-test-openai
|
||||
model=qwen3-235b max_tokens=8192 thinking=true tools=["inventory__query"] stream=true
|
||||
```
|
||||
|
||||
配置字段:
|
||||
|
||||
- `Model`:`id`(上游模型名)、`api`(`anthropic-messages` | `openai-completions` | `openai-responses` | …)、`provider`、`baseUrl`、`reasoning`、`thinkingLevelMap`、`contextWindow`、`maxTokens`、`cost`、`headers`、`compat`(OpenAI 兼容上游的差异开关,如 `supportsDeveloperRole`、`thinkingFormat`、`maxTokensField`)。
|
||||
- baseUrl 的拼接方式:Anthropic 写 **不带 `/v1`** 的根地址(SDK 自己拼 `/v1/messages`);OpenAI 兼容写 **带 `/v1`** 的地址(SDK 拼 `/chat/completions`)。
|
||||
- apiKey:`createProvider({ auth: { apiKey: { name, resolve: async () => ({ auth: { apiKey, baseUrl?, headers? } }) } } })`,每次请求时解析,可以接我们自己的 secret store。内置 provider 用 `envApiKeyAuth` 读环境变量(如 `ANTHROPIC_API_KEY`),也可以用 `createModels({ credentials })` 注入。
|
||||
- 思考档位:thinkingLevel 映射到上游参数(Anthropic high → `budget_tokens 7168`),可用 `getSupportedThinkingLevels(model)` / `clampThinkingLevel` 查询。
|
||||
- 上下文预算:`model.contextWindow`(可以自定义成比上游小的值)配合 `compaction.reserveTokens/keepRecentTokens`。
|
||||
|
||||
## 3. 不支持 / 未验证
|
||||
|
||||
- **子代理**:pi-agent-core 和 pi-ai 里都没有子代理概念(dist 里 grep 不到)。设置页的"子代理开关"要由我们自己实现,例如在工具里开一个新 lane 或新会话,并继承预算和授权。
|
||||
- **工具名不能带点**:`inventory.query` 这种名字上游(Anthropic/OpenAI 要求 `^[a-zA-Z0-9_-]{1,64}$`)不接受,本 spike 统一映射成 `inventory__query`。pi 自身是否校验名字**未验证**。
|
||||
- **PostgreSQL 会话存储**:0.87.1 只提供 Memory / JSONL / SQLite(独立包 `pi-session-backend-sqlite-node`,**未验证**)。Postgres 分区存储在规范 Part 6 里标注为 future。存储格式 4 仍是 WIP,不保证迁移。
|
||||
- `watchSession` 未实现(规范 R12);遥测只有部分实现。
|
||||
- 模型流式输出中途崩溃的恢复(合成 error 后按 retry 重试)只读了规范,**没有实测**。
|
||||
- pi-coding-agent 的 `createAgentSession` / ResourceLoader **没有安装,也没有运行**(按上文结论不采用)。
|
||||
- 与真实上游联网调用:没有(不需要 key)。
|
||||
Reference in new issue
Block a user