diff --git a/docs/adr/ADR-002-task-engine.md b/docs/adr/ADR-002-task-engine.md new file mode 100644 index 00000000..b18dd37a --- /dev/null +++ b/docs/adr/ADR-002-task-engine.md @@ -0,0 +1,112 @@ +# ADR-002:持久任务引擎采用 OpenWorkflow 0.10.1(锁版本),封装在 `TaskEngine` 端口之后 + +- 状态:已采纳(2026-09-27,W0-B spike 后) +- 关联:REQ §11、docs/plan/00-first-version-plan.md §1 与 §3.1-8、验证记录 `docs/spikes/w0b-openworkflow.md`、代码 `spikes/openworkflow/` + +## 决定 + +v0.1 采用 **`openworkflow@0.10.1`(精确锁版本)+ `postgres@^3.4.9`**,表放在同库独立 schema `openworkflow`。所有调用都经过 `packages/tasks` 的 `TaskEngine` 端口,业务代码不 import `openworkflow`。**业务状态不放在引擎里**:引擎只负责"过程推进、定时唤醒、重试、租约";下载任务状态、授权、操作记录都在业务表里。 + +暂不实现自研租约任务表。下文附录给出备选设计,只在触发复议条件时启用(REQ §11.1:"只保留清晰的执行接口,不提前实现第二套引擎")。 + +## 验证结论摘要 + +| 项 | 结果 | 对接入的约束 | +| --- | --- | --- | +| (a) 表初始化 | `BackendPostgres.connect()` 自动幂等迁移 | 迁移放进统一迁移流程;启动时 `runMigrations:false` | +| (b) kill -9 恢复 | 通过。已完成步骤不重跑,正在执行的步骤会在新 worker 上**重跑**(最长等 30s 租约到期) | 步骤体必须幂等 | +| 租约丢失 | 引擎拒收过期 worker 的步骤结果,但**步骤体仍会执行完并产生副作用**,拿不到租约状态 | 业务写入要用操作账本做 fencing | +| (c) 信号提前到达 | **丢失**。没有等待者时 `sendSignal` 直接返回 `[]`,不落库 | 授权、下载完成都**先写业务库**,信号只用来加速唤醒 | +| (d) 取消 | run 变成 `canceled`,后续步骤不再执行;**进行中的步骤不中断**(没有 AbortSignal);已取消的 run 仍会"接收"信号 | 长步骤自己轮询业务侧取消标志;补偿策略显式化 | +| (e) 重试 | 步骤级 `maximumAttempts` 生效,超过后 run 变成 `failed` | 默认 10 次偏多,按任务类型显式配置 | +| (f) 幂等键 | 同 key 返回已有 run;**24h 窗口,不比较输入** | 幂等键由服务端按语义生成,长期去重靠业务表唯一约束 | +| (g) 版本升级 | 旧版本 run 找不到对应 version 的 worker 时无限退避等待,不会失败;同一 version 改代码按步骤名重放,不做确定性检查 | 不兼容的改动必须升 version,worker 同时注册新旧版本 | +| (h) 与 Kysely 共库 | 不同 schema,无冲突;引擎用自己的 postgres.js 连接池,**不能和业务写入同事务入队** | outbox | +| (i) 双 worker 并发 | 60 个 run 执行 60 次,无重复(`SKIP LOCKED`) | — | +| PG 重启 | 自动恢复 | — | + +## 给 `packages/tasks` 的接入要求 + +### 1. 端口(业务只依赖它) + +```ts +export interface TaskEngine { + /** 幂等:同 kind+idempotencyKey 返回同一 taskRunId。调用方负责先在业务事务里写 outbox。 */ + enqueue(kind: K, input: TaskInput, opts: { idempotencyKey: string; availableAt?: Date }): Promise<{ taskRunId: string }>; + /** 只做唤醒提示。等待方必须以业务库为准;信号丢了也不能影响正确性。 */ + signal(topic: SignalTopic, payload?: JsonValue): Promise<{ delivered: number }>; + /** 停止后续步骤。不撤销已完成的外部动作,也不打断进行中的步骤。 */ + cancel(taskRunId: string, reason: string): Promise; + get(taskRunId: string): Promise; // 状态/attempts/error/当前步骤,只读投影 + start(opts: { roles: ("worker")[]; concurrency: number }): Promise; + stop(): Promise; +} +``` + +`TaskKind` 对应 plan §3.1-8 的九类任务。每类任务在 `packages/tasks` 里定义 `{ name, version, retryPolicy, handler }`,handler 只能通过注入的业务服务读写 Kysely。 + +### 2. 必须遵守的模式 + +1. **Outbox 入队**:业务事务里写 `outbox_event(id, kind, idempotency_key, payload, status, attempts, next_attempt_at)`;分发器轮询 `pending` 行,调用 `enqueue`(引擎幂等键 = outbox 的 idempotency_key),成功后标记 `dispatched`。重复分发由 24h 引擎幂等和业务唯一约束兜底。 +2. **等待外部条件 = 查库 + 带超时的信号**。不要只等一个信号: + ```ts + while (true) { + const ok = await step.run({ name: `check-approval-${i}` }, () => approvals.findValid(planId)); // 业务库是唯一真相 + if (ok) break; + await step.waitForSignal({ name: `wait-approval-${i}`, signal: `approval:${planId}`, timeout: "5m" }); // 信号只加速 + i++; + } + ``` + 注意:同一信号名同一时间只能有一个等待步骤;步骤名要带序号(引擎也会自动加 `:1` 之类的后缀)。每轮循环都消耗 step attempt 配额(单个 run 上限 1000),长期等待用 `timeout` 拉长间隔,例如下载完成用 10m。 +3. **副作用步骤走操作账本**(REQ §11.2):`asset_operation`/`download_task` 记录 `plan → attempt(started) → result`,步骤体开头先查账本: + - 已有 result:直接返回,不重做; + - 有 attempt 没有 result:先对账外部状态(qB 按 hash 或管理标签查询,文件按目标路径 + 大小/hash 核验),再决定收尾还是重做; + - 提交业务状态时带 fencing 条件,例如 `UPDATE … WHERE id=? AND attempt_token=?`。attempt_token 在步骤开始时写入,重跑的新 attempt 会覆盖它,老 worker 的提交因此失败。 +4. **取消**:`TaskEngine.cancel` = 先写业务侧 `cancel_requested`,再调用引擎 cancel。长步骤(复制大文件)分块时检查这个标志,自己中止;补偿(暂停 qB、删临时文件)作为显式步骤或单独任务。 +5. **版本纪律**:handler 有不兼容改动时 `version` +1;worker 同时注册当前和上一个版本,直到旧版本的 run 清空(用 `listWorkflowRuns({ workflowName, status })` 查,并在 `/admin` 显示)。同版本内只允许在末尾追加步骤,或插入幂等步骤,不允许改已有步骤名。 +6. **重试策略显式化**:每类任务显式给出 `retryPolicy`。外部网络类 5 次,1s→60s;文件类 3 次。用尽后 run 变成 `failed`,由 `resolveDecision`/待处理中心接管。死信视图 = `status='failed'` 的 run 加业务上下文。 +7. **展示**:UI 的业务时间线读业务表,不读 `step_attempts`,因为里面会有停在 `running` 的孤儿行。 +8. **连接**:引擎用自己的 postgres.js 连接池(同一个 DATABASE_URL);Kysely 用 pg。两边的连接数都计入 PG `max_connections`。 + +### 3. 测试要求 + +- vitest 集成测试(testcontainers 或 docker PG):kill worker 后续跑、提前信号 + 查库路径、取消、重试上限、幂等 enqueue、双 worker。 +- 故障注入(REQ §11.1):qB 接收后断连、复制完成但结果未提交、DB 断连、重复投递、租约丢失(SIGSTOP)、运行中升级。断言对象是**业务账本和文件系统**,不是引擎 `completed`。 + +## 已知风险 + +- 0.x 版本,API 仍在变(0.1→0.10 一年内发布 25 个版本)。锁精确版本,升级前重跑 spike 用例。 +- 租约固定 30s,不可配置;worker 挂掉后最长约 30s 才会续跑。 +- 步骤体拿不到 AbortSignal 和租约状态,fencing 全靠业务层。 +- 信号不持久,同名只能有一个等待者。 +- 引擎自带的 idempotency 只有 24h。 + +## 复议条件(触发后启用附录中的自研表) + +- 升级到新版本后恢复/重试语义回归,且短期修不好; +- 需要与业务写入同事务入队,且 outbox 延迟不可接受; +- 需要可配置租约或步骤级取消信号,但上游不支持。 + +## 附录:备选的自研租约任务表(本版不实现) + +```sql +create table task ( + id uuid primary key, kind text not null, handler_version int not null, + idempotency_key text not null, input jsonb not null, state jsonb not null default '{}', -- 步骤游标 + 已完成步骤结果 + status text not null check (status in ('pending','running','waiting','succeeded','failed','canceled','dead')), + attempts int not null default 0, max_attempts int not null, + next_run_at timestamptz not null default now(), lease_owner text, lease_until timestamptz, lease_token uuid, + wait_topic text, wait_until timestamptz, cancel_requested_at timestamptz, + last_error jsonb, dead_letter_at timestamptz, created_at timestamptz default now(), updated_at timestamptz default now(), + unique (kind, idempotency_key) +); +create index on task (status, next_run_at); +create table task_signal (id bigserial primary key, topic text not null, payload jsonb, created_at timestamptz default now(), consumed_by uuid); +-- 信号持久化:等待方进入 waiting 前先查未消费的 task_signal,解决"提前到达" +-- outbox_event 同 §2.1 +``` + +- 认领:`UPDATE task SET status='running', lease_owner=$w, lease_token=gen_random_uuid(), lease_until=now()+$lease, attempts=attempts+1 WHERE id=(SELECT id FROM task WHERE status IN('pending','waiting') AND next_run_at<=now() AND cancel_requested_at IS NULL ORDER BY priority, next_run_at FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING *`。 +- 心跳:`UPDATE … SET lease_until=… WHERE id=$id AND lease_token=$tok`,影响 0 行说明租约已丢,立即中止并禁止提交。所有业务提交都带 `lease_token` 条件,在同一事务里完成。 +- 恢复算法:扫描 `status='running' AND lease_until; // 建会话 + accept prompt,不驱动 + stream(agentRunId: string, fromSeq?: number): AsyncIterable; // SSE 用,事件先落库(带 seq)再推送 + drive(agentRunId: string, signal?: AbortSignal): Promise; // 由持久任务调用;内部是 lane.resume / drive + abort(agentRunId: string, reason: string): Promise; // lane.abort → 工具收到 context.abortSignal + resume(agentRunId: string): Promise; // 进程重启后:AgentHarness.create 返回 open → lane.resume + followUp(agentRunId: string, message: string): Promise; // 用户纠正 +} +``` + +实现要点: + +- `drive/resume` 由 `packages/tasks` 的持久任务(例如 `agentRun` 任务)调用。聊天 HTTP 只做 createRun + stream,断线不影响执行(REQ §10.7)。 +- 事件监听器(`harness.events.on`)把事件写入 `agent_run_event`(seq 单调递增),SSE 按 seq 补读。 +- 预算:`usage` 事件累计,`before_request` hook 里发现超预算就 `abort`,状态记为 `budget_exceeded`,然后进入 attention。 +- 会话存储:v0.1 用 `JsonlSessionRepo`,目录固定为应用数据目录 `data/agent-sessions/`(不放在媒体目录或用户目录),`agent_run` 行记录 session id 和路径。`NodeExecutionEnv` 只当作这个目录的 FileSystem 用,**不把它的 exec/shell 暴露成工具**。PG 存储要等上游或自研 `Storage` 实现(接口约 10 个方法);格式仍是 WIP,所以不在 v0.1 做。 +- 业务真相放在 PG(`agent_run`、操作账本、能力调用记录),JSONL 只是会话转写和恢复点。REQ §10.5/§11.4 说"不能把压缩摘要当唯一状态",这里同理。 + +## 工具白名单机制(三层) + +1. **注册层**:`AgentHarness.create({ tools })` 只传本次 `capabilities` 对应的工具;也可以用 `activeToolNames` / `lane.setActiveTools` 缩小范围。模型调用未注册的工具时,harness 合成 `Tool "x" is unavailable` 错误结果,不会执行(spike 中已验证 `bash`)。 +2. **授权层**:工具 `execute` 调能力服务。能力服务在服务端校验 `toolContext`(actorId、grants、scope)和持续/单次授权(REQ §10.3),不信任模型参数里的"已同意"。`before_tool` hook 用来记审计和做快速拒绝(`{ block: { reason } }`)。 +3. **命名映射**:能力 ID `download.submit` ↔ 工具名 `download__submit`,因为上游工具名只允许 `[a-zA-Z0-9_-]`。映射表在 `packages/agent` 里唯一定义。 + +每个能力还要声明 `replay`:只读能力(`catalog.lookup`、`inventory.query`、`task.inspect` 等)用 `"safe"`;有副作用的能力(`download.submit`、`organize.execute`、`policy.apply` 等)用 `"never"`。崩溃恢复时,副作用工具会得到"结果未知"。业务侧按服务端生成的幂等键到操作账本对账,模型重新调用时会命中去重,不会产生第二次副作用。 + +## fake provider 用法 + +```ts +const faux = fauxProvider({ provider: "umt-fake", models: [{ id: "fake", contextWindow: 32000 }] }); +models.setProvider(faux.provider); +faux.setResponses([ + () => fauxAssistantMessage([fauxText("先查库存"), fauxToolCall("inventory__query", { title: "Dune" })], { stopReason: "toolUse" }), + (ctx) => /* 读 ctx.messages 里的 toolResult 决定下一步 */ fauxAssistantMessage("完成"), +]); +// faux.state.callCount、getPendingResponseCount();队列耗尽时返回 errorMessage "No more faux responses queued" +``` + +`packages/agent` 提供 `createScriptedModel(steps)`,供 testkit 和 e2e 使用(plan §3.1-13,"把 X 电影 1080p 收进来")。 + +## 设置页 → SDK 映射 + +| 设置项 | SDK | +| --- | --- | +| 上游接入(Anthropic / OpenAI 兼容) | `createProvider({ id, auth: { apiKey: { resolve } }, models: [Model], api: anthropicMessagesApi() / openAICompletionsApi() })`;`Model.baseUrl`(Anthropic 不带 `/v1`,OpenAI 兼容带 `/v1`)、`Model.id`、`Model.compat` | +| 模型选择 | `lane.setModel({ provider, modelId })` | +| 思考档位 | `thinkingLevel` / `lane.setThinkingLevel`;可选值来自 `getSupportedThinkingLevels(model)` | +| 上下文预算 | 自定义 `Model.contextWindow` / `maxTokens` | +| 压缩策略 | `compaction: { enabled, reserveTokens, keepRecentTokens }` + `before_compaction` hook / `customInstructions`(保留目标、约束、证据引用、已完成动作、剩余事项) | +| 重试 | `retry: { enabled, maxRetries, baseDelayMs }`、`streamOptions.timeoutMs` | +| 子代理开关 | **SDK 不提供**。由我们实现一个 `agent.delegate` 能力:开新 lane 或新会话,继承父任务的授权上限、预算和取消状态;默认关闭 | + +## 已知限制 + +- 0.x 版本,存储格式 4 仍是 WIP,可能原地改变。锁精确版本,升级前重跑 `spikes/pi`。 +- 没有子代理,没有 PG 会话存储,`watchSession` 未实现。 +- Pi 不提供权限系统(REQ §10.2)。进程、容器、网络白名单要在部署层落实。 +- 模型流中途崩溃后按"合成 error + 重试"恢复,只读了规范,未实测;v0.1 的 e2e 要补上。 +- 真实上游只验证了请求形状(本地抓包),没有联网调用。 diff --git a/docs/spikes/w0b-openworkflow.md b/docs/spikes/w0b-openworkflow.md new file mode 100644 index 00000000..1a480c03 --- /dev/null +++ b/docs/spikes/w0b-openworkflow.md @@ -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)、`openworkflow@0.10.1`、`postgres@3.4.x`、`kysely@0.28` + `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({ 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)。 diff --git a/docs/spikes/w0b-pi.md b/docs/spikes/w0b-pi.md new file mode 100644 index 00000000..c9ba5b08 --- /dev/null +++ b/docs/spikes/w0b-pi.md @@ -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/pi-agent-core@0.87.1`、`@earendil-works/pi-ai@0.87.1`(依赖 `@earendil-works/chord`、`pi-telemetry`、`typebox@1.3.27`) +- 代码:`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 = { + 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)。 diff --git a/spikes/.gitignore b/spikes/.gitignore new file mode 100644 index 00000000..065b8d47 --- /dev/null +++ b/spikes/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +pi/.sessions/ diff --git a/spikes/README.md b/spikes/README.md new file mode 100644 index 00000000..04202795 --- /dev/null +++ b/spikes/README.md @@ -0,0 +1,15 @@ +# W0-B spikes(独立 npm 包,不是 pnpm workspace 成员) + +```bash +cd spikes && npm install +docker run -d --name umt-spike-pg -e POSTGRES_PASSWORD=spike -p 55434:5432 postgres:16-alpine +``` + +- `openworkflow/`:OpenWorkflow 0.10.1 纵切(提交下载→等信号→复制→登记→保留期 sleep→清理)。 + `worker.ts`(env: TAG / WF_VERSION / EXTRA_STEP / PIDFILE)、`cli.ts`(start-acquire / signal / cancel / get / steps / ledger / start-flaky / start-slow / start-burst / burst-dups / tables)。 + 记录:`../docs/spikes/w0b-openworkflow.md`,结论 `../docs/adr/ADR-002-task-engine.md`。 +- `pi/`:pi-agent-core/pi-ai 0.87.1。`fake-run.ts`(脚本化 fake 模型 + 2 个工具 + 事件流)、 + `abort-resume.ts abort|crash|resume`(取消 / kill -9 后新进程恢复)、`compaction.ts`、`provider-config.ts`(真实 provider 类型打到本地抓包服务器,不出网)。 + 记录:`../docs/spikes/w0b-pi.md`,结论 `../docs/adr/ADR-003-pi-integration.md`。 + +运行:`npx tsx openworkflow/cli.ts tables`、`npx tsx pi/fake-run.ts` 等。 diff --git a/spikes/openworkflow/cli.ts b/spikes/openworkflow/cli.ts new file mode 100644 index 00000000..69532f0d --- /dev/null +++ b/spikes/openworkflow/cli.ts @@ -0,0 +1,64 @@ +import { makeClient, initBiz, db } from "./common.ts"; +import { sql } from "kysely"; +await initBiz(); +const { ow, backend, acquire, flaky, slow, burst } = await makeClient(); +const [cmd, ...args] = process.argv.slice(2); +const out = (x: unknown) => console.log(JSON.stringify(x, null, 0)); +switch (cmd) { + case "start-acquire": { + const [downloadId, key, copyMs, retention] = args; + const h = await acquire.run({ downloadId, copyMs: Number(copyMs ?? 0), retention: retention ?? "3s" }, + key ? { idempotencyKey: key } : undefined); + out({ runId: h.workflowRun.id, status: h.workflowRun.status, version: h.workflowRun.version }); + break; + } + case "start-flaky": { + const h = await flaky.run({ failTimes: Number(args[0]), maxAttempts: Number(args[1]) }); + out({ runId: h.workflowRun.id }); + break; + } + case "start-slow": { const h = await slow.run({}); out({ runId: h.workflowRun.id }); break; } + case "start-burst": { + const ids = []; + for (let i = 0; i < Number(args[0]); i++) ids.push((await burst.run({ i })).workflowRun.id); + out({ count: ids.length }); + break; + } + case "signal": { + const r = await ow.sendSignal({ signal: `download-done:${args[0]}`, data: { ok: true }, idempotencyKey: args[1] }); + out(r); + break; + } + case "cancel": { await ow.cancelWorkflowRun(args[0]); out({ canceled: args[0] }); break; } + case "get": { + const r = await backend.getWorkflowRun({ workflowRunId: args[0] }); + out({ status: r?.status, version: r?.version, attempts: r?.attempts, error: r?.error, output: r?.output }); + break; + } + case "steps": { + const r = await backend.listStepAttempts({ workflowRunId: args[0], limit: 100 } as any); + for (const s of [...r.data].sort((a,b)=>+a.createdAt-+b.createdAt)) console.log(s.stepName.padEnd(18), s.kind.padEnd(12), s.status, s.error ? JSON.stringify(s.error.message) : ""); + break; + } + case "ledger": { + const rows = await db.selectFrom("biz.side_effect_ledger").selectAll() + .$if(!!args[0], (q) => q.where("run_id", "=", args[0])).orderBy("id").execute(); + for (const r of rows) console.log(r.run_id.slice(0, 8), r.step.padEnd(16), r.worker.padEnd(6), r.note); + break; + } + case "burst-dups": { + const rows = await sql`SELECT count(*) total, count(DISTINCT run_id) runs, + count(*) FILTER (WHERE worker='A') a, count(*) FILTER (WHERE worker='B') b + FROM biz.side_effect_ledger WHERE step='work'`.execute(db); + out(rows.rows[0]); + break; + } + case "tables": { + const r = await sql`SELECT table_schema, table_name FROM information_schema.tables + WHERE table_schema IN ('openworkflow','biz') ORDER BY 1,2`.execute(db); + for (const t of r.rows) console.log(`${t.table_schema}.${t.table_name}`); + break; + } +} +await backend.stop(); +await db.destroy(); diff --git a/spikes/openworkflow/common.ts b/spikes/openworkflow/common.ts new file mode 100644 index 00000000..94d0ed44 --- /dev/null +++ b/spikes/openworkflow/common.ts @@ -0,0 +1,110 @@ +// Shared: DB URLs, OpenWorkflow client, Kysely business DB (schema "biz"), workflow definitions. +import { OpenWorkflow } from "openworkflow"; +import { BackendPostgres } from "openworkflow/postgres"; +import { Kysely, PostgresDialect, sql } from "kysely"; +import pg from "pg"; + +export const PG_URL = process.env.PG_URL ?? "postgresql://postgres:spike@127.0.0.1:55434/postgres"; +export const WF_VERSION = process.env.WF_VERSION ?? "v1"; +export const TAG = process.env.TAG ?? `pid${process.pid}`; + +export const db = new Kysely({ + dialect: new PostgresDialect({ pool: new pg.Pool({ connectionString: PG_URL, max: 4 }) }), +}); + +export async function initBiz() { + await sql`CREATE SCHEMA IF NOT EXISTS biz`.execute(db); + await sql`CREATE TABLE IF NOT EXISTS biz.side_effect_ledger ( + id bigserial PRIMARY KEY, run_id text, step text, worker text, pid int, note text, + at timestamptz DEFAULT now())`.execute(db); +} + +export async function ledger(runId: string, step: string, note = "") { + await db.insertInto("biz.side_effect_ledger") + .values({ run_id: runId, step, worker: TAG, pid: process.pid, note }).execute(); + console.log(`[${TAG}] run=${runId.slice(0, 8)} step=${step} ${note}`); +} + +const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export async function makeClient() { + // connect() runs migrations by default (runMigrations: true), schema default "openworkflow". + const backend = await BackendPostgres.connect(PG_URL, { schema: "openworkflow" }); + const ow = new OpenWorkflow({ backend }); + + // Main vertical slice: submit download -> wait signal -> copy -> register -> retention sleep -> cleanup + const acquire = ow.defineWorkflow( + { name: "acquire", version: WF_VERSION }, + async ({ input, step, run, version }) => { + const { downloadId, copyMs = 0, retention = "3s" } = input as any; + const sub = await step.run({ name: "submit-download" }, async () => { + await ledger(run.id, "submit-download", `version=${version}`); + return { torrentHash: `hash-${downloadId}` }; + }); + const done = await step.waitForSignal<{ ok: boolean }>({ + name: "wait-download", signal: `download-done:${downloadId}`, timeout: "10m", + }); + if (!done) throw new Error("download wait timed out"); + const copied = await step.run({ name: "copy-file" }, async () => { + await ledger(run.id, "copy-file", "start"); + await wait(copyMs); + await ledger(run.id, "copy-file", "end"); + return { dest: `/lib/${downloadId}.mkv` }; + }); + if (process.env.EXTRA_STEP === "1") { + // EXTRA_STEP=1 simulates a code change between copy and register (tests code upgrade semantics) + await step.run({ name: "v2-extra-verify" }, async () => { await ledger(run.id, "v2-extra-verify"); return 1; }); + } + await step.run({ name: "register" }, async () => { await ledger(run.id, "register", `version=${version}`); return 1; }); + await step.sleep("retention", retention); + await step.run({ name: "cleanup" }, async () => { await ledger(run.id, "cleanup"); return 1; }); + return { sub, copied, signal: done.data }; + }, + ); + + // Retry test: step fails `failTimes` times then succeeds; step maximumAttempts = maxAttempts + const flaky = ow.defineWorkflow( + { name: "flaky", version: WF_VERSION }, + async ({ input, step, run }) => { + const { failTimes, maxAttempts } = input as any; + await step.run({ name: "before" }, async () => { await ledger(run.id, "before"); return 1; }); + return await step.run( + { name: "flaky-step", retryPolicy: { maximumAttempts: maxAttempts, initialInterval: "200ms", maximumInterval: "1s" } }, + async () => { + const n = await db.selectFrom("biz.side_effect_ledger").select(db.fn.countAll().as("c")) + .where("run_id", "=", run.id).where("step", "=", "flaky-step").executeTakeFirst(); + const attempt = Number(n?.c ?? 0) + 1; + await ledger(run.id, "flaky-step", `attempt=${attempt}`); + if (attempt <= failTimes) throw new Error(`boom attempt ${attempt}`); + return { attempt }; + }, + ); + }, + ); + + // Cancel test: long step, then a follow-up step + const slow = ow.defineWorkflow( + { name: "slow", version: WF_VERSION }, + async ({ step, run }) => { + await step.run({ name: "long" }, async () => { + await ledger(run.id, "long", "start"); + await wait(8000); + await ledger(run.id, "long", "end (still ran after cancel?)"); + return 1; + }); + await step.run({ name: "after-long" }, async () => { await ledger(run.id, "after-long"); return 1; }); + return "done"; + }, + ); + + // Concurrency test: many short runs + const burst = ow.defineWorkflow( + { name: "burst", version: WF_VERSION }, + async ({ step, run }) => { + await step.run({ name: "work" }, async () => { await wait(300); await ledger(run.id, "work"); return 1; }); + return 1; + }, + ); + + return { ow, backend, acquire, flaky, slow, burst }; +} diff --git a/spikes/openworkflow/worker.ts b/spikes/openworkflow/worker.ts new file mode 100644 index 00000000..983e906b --- /dev/null +++ b/spikes/openworkflow/worker.ts @@ -0,0 +1,8 @@ +import { makeClient, initBiz, TAG, WF_VERSION } from "./common.ts"; +await initBiz(); +const { ow } = await makeClient(); +const worker = ow.newWorker({ concurrency: Number(process.env.CONCURRENCY ?? 4) }); +await worker.start(); +if (process.env.PIDFILE) (await import("node:fs")).writeFileSync(process.env.PIDFILE, String(process.pid)); +console.log(`[${TAG}] worker started pid=${process.pid} version=${WF_VERSION}`); +process.on("SIGTERM", async () => { await worker.stop(); process.exit(0); }); diff --git a/spikes/package-lock.json b/spikes/package-lock.json new file mode 100644 index 00000000..831f9ef8 --- /dev/null +++ b/spikes/package-lock.json @@ -0,0 +1,1846 @@ +{ + "name": "umt-spikes", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "umt-spikes", + "dependencies": { + "@earendil-works/pi-agent-core": "0.87.1", + "@earendil-works/pi-ai": "0.87.1", + "kysely": "^0.28.0", + "openworkflow": "0.10.1", + "pg": "^8.13.0", + "postgres": "^3.4.9", + "tsx": "^4.20.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.124.0.tgz", + "integrity": "sha512-cN5O8i9UVxHeOQAzj/XjshWXG8KiibJDw9OGpH2Z/eR3n/RBxdoLxDJOcfqAJWvjaMDFfHTBADU04hWRJVkDyA==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1127.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1127.0.tgz", + "integrity": "sha512-IDl/lrPb90aH+pZFHGNDmgH9nAUQj5PlZH1sJ3w7RikctyjHSnY3oNjZhrLoaBoQn/rNK0zsP6OHEqEhj2tdLA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/credential-provider-node": "^3.972.82", + "@aws-sdk/eventstream-handler-node": "^3.972.34", + "@aws-sdk/middleware-eventstream": "^3.972.29", + "@aws-sdk/middleware-websocket": "^3.972.52", + "@aws-sdk/token-providers": "3.1127.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.978.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz", + "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.35.0", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz", + "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz", + "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz", + "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/credential-provider-env": "^3.972.72", + "@aws-sdk/credential-provider-http": "^3.972.74", + "@aws-sdk/credential-provider-login": "^3.972.79", + "@aws-sdk/credential-provider-process": "^3.972.72", + "@aws-sdk/credential-provider-sso": "^3.973.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.79", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz", + "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.84", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz", + "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.72", + "@aws-sdk/credential-provider-http": "^3.972.74", + "@aws-sdk/credential-provider-ini": "^3.973.17", + "@aws-sdk/credential-provider-process": "^3.972.72", + "@aws-sdk/credential-provider-sso": "^3.973.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz", + "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz", + "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/token-providers": "3.1138.0", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1138.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz", + "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz", + "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.35.tgz", + "integrity": "sha512-a8xilRoRaalvSPZdfrs0VY3/BPc0uYhXW56Z/k6nmZ5Vk430LHeowf9APIPq8utM3tMJSNGSSuJMlB9YrPsN+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.30.tgz", + "integrity": "sha512-B6gvZlcnRBNWraKgyEjKsbhv+VjtbyHmpU7hmtTHzXpDSxHPpCJnYLpglEOOsF+SFne20DbZC5IVC1aNr2Pb4A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.54.tgz", + "integrity": "sha512-bhESdru8u8KosziH8jcVta/NM/dNreeRz2+2aU86si5bfBYOiJ6IiqD+9U4URUcU5WJO3Yw+jO3GLDIHlscXMQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz", + "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.47", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz", + "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1127.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1127.0.tgz", + "integrity": "sha512-Dv2TMWBshJ+tF6ahs2Sy5bh4Iabsd4GAQqVvE9XZmYmnoaVbpS2QKIKE/HRacc7bTtbjEEvP+laGzHvHlf1CiQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/nested-clients": "^3.997.44", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/chord": { + "version": "0.87.1", + "resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.87.1.tgz", + "integrity": "sha512-bg7IkJGFcEaMqqYgOGUiq5Ky9RghpRfrlZ8I/v/1b4bBZ02A7t3E+6uhPRbadwWb/kWsnVFbZsqOKRN4a3LLCg==", + "license": "MIT", + "dependencies": { + "esbuild": "0.28.2" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-agent-core": { + "version": "0.87.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.87.1.tgz", + "integrity": "sha512-Zev3B0HK7YS5A4EZQ2XnEqiJuirx6QBiltJ+LpmjV5a/+2IU0cfKtIfnkNkORK707XOvKBY2WRtk7cAwHpbh2Q==", + "license": "MIT", + "dependencies": { + "@earendil-works/chord": "^0.87.1", + "@earendil-works/pi-ai": "^0.87.1", + "@earendil-works/pi-telemetry": "^0.87.1", + "diff": "8.0.4", + "ignore": "7.0.8", + "typebox": "1.3.27", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.87.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.87.1.tgz", + "integrity": "sha512-X/3PfQBnnoeVdO9Cv8zHghUMglzlgNZYGNzoPnbRoGnHl3Rw3TlA2UKSUB7BRHUOxMryHXYa8dnjWZlbRheDZA==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.124.0", + "@aws-sdk/client-bedrock-runtime": "3.1127.0", + "@earendil-works/pi-telemetry": "^0.87.1", + "@google/genai": "2.21.0", + "@smithy/node-http-handler": "4.12.1", + "http-proxy-agent": "9.1.0", + "https-proxy-agent": "9.1.0", + "openai": "6.40.0", + "partial-json": "0.1.7", + "typebox": "1.3.27" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-telemetry": { + "version": "0.87.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.87.1.tgz", + "integrity": "sha512-MC6TRQH5lgMXpcN+Vku2WMI2T8BsiUPzMQHGo81uqFZD3/9O79WWJAysEDGuzduP6R4tvtgwMLwmqIxynM10JQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.21.0.tgz", + "integrity": "sha512-+PDtco2/Z0ONdzCGekCoCT+O1VJS9xJQNN4XzQpXG/t3El/SWWMkCWlFRO1KmivOHPa4Q0VjUYu1HBKCZ/v33Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/core": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz", + "integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", + "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kysely": { + "version": "0.28.17", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz", + "integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/openai": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", + "license": "Apache-2.0", + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openworkflow": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/openworkflow/-/openworkflow-0.10.1.tgz", + "integrity": "sha512-BdgEjoaazhL26JgQ/SehD/tLVEUYsHD6oZWU79+04jnxc+hPlxmRRfVfaAGeUdS2YEPFr/53C0kcRNodzAdNnA==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@opentelemetry/api": "1", + "postgres": "^3.4.9" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "postgres": { + "optional": true + } + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", + "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/standardwebhooks": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz", + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typebox": { + "version": "1.3.27", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.27.tgz", + "integrity": "sha512-zu+jc1pcy4UiNThxikUr36f0Rybk9PEeCg/NE6adeWr/SKsdNO4EzZHYRDlv2YCVAfj3Odq3dESSo/jNyoBXzA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/spikes/package.json b/spikes/package.json new file mode 100644 index 00000000..e34ba4d1 --- /dev/null +++ b/spikes/package.json @@ -0,0 +1,14 @@ +{ + "name": "umt-spikes", + "private": true, + "type": "module", + "dependencies": { + "openworkflow": "0.10.1", + "postgres": "^3.4.9", + "kysely": "^0.28.0", + "pg": "^8.13.0", + "@earendil-works/pi-agent-core": "0.87.1", + "@earendil-works/pi-ai": "0.87.1", + "tsx": "^4.20.0" + } +} diff --git a/spikes/pi/abort-resume.ts b/spikes/pi/abort-resume.ts new file mode 100644 index 00000000..029fc14e --- /dev/null +++ b/spikes/pi/abort-resume.ts @@ -0,0 +1,60 @@ +// Modes: abort | crash | resume. Uses a file-backed JSONL session so a second process can recover. +import { createModels, fauxProvider } from "@earendil-works/pi-ai"; +import { AgentHarness, BACKGROUND_CONTEXT, JsonlSessionRepo } from "@earendil-works/pi-agent-core"; +import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/harness/env/nodejs"; +import { downloadSubmit, inventoryQuery, script, submitted } from "./common.ts"; + +const mode = process.argv[2]; +const ctx = BACKGROUND_CONTEXT; +const faux = fauxProvider({ provider: "umt-fake" }); +const models = createModels(); +models.setProvider(faux.provider); +faux.setResponses(mode === "resume" ? [script[2]] : script); + +const root = new URL("./.sessions", import.meta.url).pathname; +const fs = new NodeExecutionEnv({ cwd: root }); // used only as FileSystem for session storage; no shell tool is exposed +const repo = new JsonlSessionRepo({ fileSystem: fs, sessionsRoot: root }); +let session; +if (mode === "resume") { + const list = await repo.list({ cwd: root }, ctx); + const meta = list.find((m) => m.id === "crash-demo"); + console.log("found session", meta?.id, meta?.path); + session = await repo.open(meta!, ctx); +} else { + session = await repo.create({ id: mode === "crash" ? "crash-demo" : `abort-${Date.now()}`, cwd: root }, ctx); +} + +const { harness, open } = await AgentHarness.create({ + session, models, model: faux.getModel(), systemPrompt: "媒体库助手", + tools: [inventoryQuery, downloadSubmit], toolContext: { actorId: "user:1", grants: ["download.submit"] }, + toolExecution: "sequential", +}, ctx); +console.log("open operations at attach:", JSON.stringify(open)); + +let lane = await harness.lane("main", ctx); +for (const t of ["tool_start", "tool_end", "run_end", "operation_abort", "run_resume"] as const) + harness.events.on(t as any, async (e: any) => { + console.log(`[${e.type}]${e.recovery ? " (recovery)" : ""} ${e.toolName ?? ""} ${e.status ?? ""} ${e.result ? JSON.stringify(e.result.content[0]?.text).slice(0, 220) : ""}`); + if (e.type === "tool_start" && e.toolName === "download__submit") { + if (mode === "abort") setTimeout(async () => { console.log(">> lane.abort()"); console.log("abort result:", JSON.stringify(await lane.abort(ctx))); }, 300); + if (mode === "crash") setTimeout(() => { console.log(">> SIGKILL self during download.submit"); process.kill(process.pid, "SIGKILL"); }, 300); + } + }); + +if (mode === "resume") { + const r = await lane.resume(ctx); + console.log("RESUME RESULT", JSON.stringify(r)); +} else { + const r = await lane.prompt("把《Dune》1080p 收进来", undefined, ctx); + console.log("PROMPT RESULT", JSON.stringify(r)); +} +console.log("SUBMITTED in this process", JSON.stringify(submitted), "fake calls:", faux.state.callCount); +const entries = (await lane.findEntries(undefined, ctx)).reverse(); +for (const e of entries as any[]) { + if (e.type !== "message") { console.log(" entry", e.type); continue; } + const m = e.message; + const text = m.role === "assistant" ? m.content.map((c: any) => c.type === "toolCall" ? `toolCall(${c.name})` : c.type === "text" ? c.text : `<${c.type}>`).join(" | ") + : Array.isArray(m.content) ? m.content.map((c: any) => c.text).join("") : m.content; + console.log(` ${m.role.padEnd(10)} ${m.toolName ?? ""} ${m.isError ? "[isError]" : ""} ${String(text).slice(0, 200)}`); +} +await harness.close(ctx); diff --git a/spikes/pi/common.ts b/spikes/pi/common.ts new file mode 100644 index 00000000..8ddb0a63 --- /dev/null +++ b/spikes/pi/common.ts @@ -0,0 +1,83 @@ +// Shared pieces for the Pi spike: whitelisted business tools + scripted fake model. +import { Type } from "typebox"; +import { + fauxAssistantMessage, fauxText, fauxThinking, fauxToolCall, + type FauxResponseFactory, +} from "@earendil-works/pi-ai"; +import type { AgentHarnessTool } from "@earendil-works/pi-agent-core"; + +// Capability id -> provider-safe tool name. Anthropic/OpenAI tool names must match ^[a-zA-Z0-9_-]{1,64}$, +// so "inventory.query" cannot be sent verbatim; the port maps ids <-> names. +export const toolName = (capabilityId: string) => capabilityId.replace(/\./g, "__"); + +export const submitted: Array<{ key: string; title: string }> = []; + +type Ctx = { actorId: string; grants: string[] }; + +export const inventoryQuery: AgentHarnessTool = { + name: toolName("inventory.query"), + label: "inventory.query", + description: "Query local library inventory for a title. Returns whether it exists and which versions.", + parameters: Type.Object({ title: Type.String(), year: Type.Optional(Type.Number()) }), + replay: "safe", // read-only: harness may re-execute after a crash + async execute(_id, params, onUpdate, toolCtx, invocation, context) { + onUpdate({ content: [{ type: "text", text: "searching…" }], details: {} }); + const found = params.title === "Arrival"; + return { + content: [{ type: "text", text: JSON.stringify({ title: params.title, inLibrary: found, versions: found ? ["2160p"] : [] }) }], + details: { actor: toolCtx.actorId, invocationId: invocation.invocationId, aborted: context.abortSignal?.aborted ?? false }, + }; + }, +}; + +export const downloadSubmit: AgentHarnessTool = { + name: toolName("download.submit"), + label: "download.submit", + description: "Submit a download plan. Server validates authorization and generates the idempotency key.", + parameters: Type.Object({ title: Type.String(), quality: Type.String() }), + replay: "never", // external side effect: after crash the harness synthesizes an 'interrupted' result instead of re-running + executionMode: "sequential", + async execute(_id, params, _onUpdate, toolCtx, invocation, context) { + if (!toolCtx.grants.includes("download.submit")) throw new Error("not authorized: download.submit"); + // Server-side idempotency key derived from operation semantics, not from the model. + const key = `dl:${params.title}:${params.quality}`; + const delayMs = Number(process.env.SUBMIT_DELAY_MS ?? 0); + if (delayMs) { + await new Promise((resolve, reject) => { + const t = setTimeout(resolve, delayMs); + context.abortSignal?.addEventListener("abort", () => { clearTimeout(t); reject(new Error("aborted by signal")); }); + }); + } + if (!submitted.find((s) => s.key === key)) submitted.push({ key, title: params.title }); + await invocation.setMemo("submitted", key); + return { content: [{ type: "text", text: JSON.stringify({ accepted: true, idempotencyKey: key, status: "submitted" }) }], details: { key } }; + }, +}; + +const lastToolResult = (ctx: any, name: string) => + [...ctx.messages].reverse().find((m: any) => m.role === "toolResult" && m.toolName === name); + +// Scripted fake model: each step is a factory that looks at the transcript (tool results) and decides. +export const script: FauxResponseFactory[] = [ + () => fauxAssistantMessage([ + fauxThinking("先查本地库存"), + fauxText("我先查一下库里有没有《Dune》。"), + fauxToolCall(toolName("inventory.query"), { title: "Dune", year: 2021 }), + ], { stopReason: "toolUse" }), + (ctx) => { + const inv = JSON.parse(lastToolResult(ctx, toolName("inventory.query")).content[0].text); + if (inv.inLibrary) return fauxAssistantMessage("库里已有,无需下载。"); + return fauxAssistantMessage([ + fauxText("库里没有,提交 1080p 下载;顺便试一下未授权的 bash。"), + fauxToolCall(toolName("download.submit"), { title: "Dune", quality: "1080p" }), + fauxToolCall("bash", { command: "rm -rf /" }), + ], { stopReason: "toolUse" }); + }, + (ctx) => { + const dl = lastToolResult(ctx, toolName("download.submit")); + const bash = lastToolResult(ctx, "bash"); + return fauxAssistantMessage( + `下载已提交(仅代表已提交,不代表已入库):${dl.content[0].text};bash 结果 isError=${bash?.isError}。`, + ); + }, +]; diff --git a/spikes/pi/compaction.ts b/spikes/pi/compaction.ts new file mode 100644 index 00000000..b55a5028 --- /dev/null +++ b/spikes/pi/compaction.ts @@ -0,0 +1,21 @@ +// Manual + threshold compaction with the fake model (summary text is scripted). +import { createModels, fauxProvider, fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { AgentHarness, BACKGROUND_CONTEXT as ctx, MemorySessionRepo } from "@earendil-works/pi-agent-core"; +const faux = fauxProvider({ provider: "umt-fake", models: [{ id: "tiny", contextWindow: 3000, maxTokens: 500 }] }); +const models = createModels(); models.setProvider(faux.provider); +const session = await new MemorySessionRepo().create({}, ctx); +const { harness } = await AgentHarness.create({ session, models, model: faux.getModel(), systemPrompt: "助手", + compaction: { enabled: true, reserveTokens: 600, keepRecentTokens: 200 } }, ctx); +for (const t of ["compaction_start", "compaction_end", "run_end"] as const) + harness.events.on(t as any, (e: any) => console.log(`[${e.type}] reason=${e.reason ?? ""} status=${e.status ?? ""}`)); +const lane = await harness.lane("main", ctx); +const long = "目标:收录 Dune 1080p。约束:只用已授权能力。".repeat(Number(process.env.REPEAT ?? 60)); +faux.setResponses([fauxAssistantMessage("好的,已记录目标。" + "x".repeat(2000)), fauxAssistantMessage("## 摘要\n目标:收录 Dune 1080p;已完成:无"), fauxAssistantMessage("继续")]); +console.log("run1:", JSON.stringify((await lane.prompt(long, undefined, ctx)) as any).slice(0, 400)); +console.log("run2:", JSON.stringify((await lane.prompt("继续", undefined, ctx)) as any).slice(0, 400)); +faux.appendResponses([fauxAssistantMessage("## 手动摘要\n目标/约束/证据/已完成/剩余")]); +const c = await lane.compact({ customInstructions: "保留目标、约束、证据引用、已完成动作、剩余事项" }, ctx); +console.log("manual compact:", JSON.stringify(c).slice(0, 200)); +const entries = await lane.findEntries(undefined, ctx); +console.log("entry types:", entries.map((e: any) => e.type).join(","), "fake calls:", faux.state.callCount); +await harness.close(ctx); diff --git a/spikes/pi/fake-run.ts b/spikes/pi/fake-run.ts new file mode 100644 index 00000000..a4f8f489 --- /dev/null +++ b/spikes/pi/fake-run.ts @@ -0,0 +1,50 @@ +// Fake model drives AgentHarness through 2 whitelisted tools; streams events. +import { createModels, fauxProvider } from "@earendil-works/pi-ai"; +import { AgentHarness, BACKGROUND_CONTEXT, MemorySessionRepo } from "@earendil-works/pi-agent-core"; +import { downloadSubmit, inventoryQuery, script, submitted } from "./common.ts"; + +const faux = fauxProvider({ provider: "umt-fake" }); +const models = createModels(); +models.setProvider(faux.provider); +faux.setResponses(script); + +const repo = new MemorySessionRepo(); +const session = await repo.create({ id: "spike-1" }, BACKGROUND_CONTEXT); +const { harness } = await AgentHarness.create({ + session, models, model: faux.getModel(), + systemPrompt: "你是媒体库自动化助手,只能使用提供的工具。", + tools: [inventoryQuery, downloadSubmit], // explicit whitelist; no discovery + toolContext: { actorId: "user:1", grants: ["download.submit"] }, + thinkingLevel: "off", + compaction: { enabled: true, reserveTokens: 4096, keepRecentTokens: 8000 }, + retry: { enabled: true, maxRetries: 2, baseDelayMs: 100 }, + toolExecution: "sequential", +}, BACKGROUND_CONTEXT); + +// Server-side authorization gate in addition to the whitelist. +harness.hooks.on("before_tool", (ev) => { + console.log(` [hook before_tool] ${ev.toolName} args=${JSON.stringify(ev.args)}`); + return undefined; +}); + +const types = ["run_start", "turn_start", "message_update", "tool_start", "tool_update", "tool_end", "turn_end", "run_end", "usage", "fault", "handler_error"] as const; +for (const t of types) harness.events.on(t as any, (e: any) => { + if (e.type === "message_update") { + if (e.event.type === "text_delta") process.stdout.write(` [text_delta] ${JSON.stringify(e.event.delta)}\n`); + else if (e.event.type === "toolcall_end") console.log(` [toolcall_end] ${e.event.toolCall.name}`); + return; + } + if (e.type === "tool_end") return console.log(`[tool_end] ${e.toolName} isError=${e.isError} -> ${JSON.stringify(e.result.content[0]?.text)}`); + if (e.type === "tool_start") return console.log(`[tool_start] ${e.toolName} ${JSON.stringify(e.args)}`); + if (e.type === "usage") return console.log(`[usage] in=${e.row?.usage?.input ?? "?"} totals.input=${e.totals.input} output=${e.totals.output}`); + if (e.type === "run_end") return console.log(`[run_end] status=${e.status}${e.error ? " error=" + JSON.stringify(e.error) : ""}`); + console.log(`[${e.type}]${e.lane ? " lane=" + e.lane : ""}`); +}); + +const lane = await harness.lane("main", BACKGROUND_CONTEXT); +const result = await lane.prompt("把《Dune》1080p 收进来", undefined, BACKGROUND_CONTEXT); +console.log("RESULT", JSON.stringify(result)); +console.log("SUBMITTED", JSON.stringify(submitted), "fake calls:", faux.state.callCount); +const entries = await lane.findEntries(undefined, BACKGROUND_CONTEXT); +console.log("TRANSCRIPT roles:", entries.map((e: any) => e.type === "message" ? e.message.role : e.type).join(",")); +await harness.close(BACKGROUND_CONTEXT); diff --git a/spikes/pi/provider-config.ts b/spikes/pi/provider-config.ts new file mode 100644 index 00000000..5af92ca1 --- /dev/null +++ b/spikes/pi/provider-config.ts @@ -0,0 +1,69 @@ +// Real provider types (Anthropic Messages + OpenAI-compatible Chat Completions) pointed at a LOCAL capture server. +// No external network: the capture server records method/path/headers/body and returns HTTP 400. +import http from "node:http"; +import { createModels, createProvider, getSupportedThinkingLevels, type Model } from "@earendil-works/pi-ai"; +import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy"; +import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy"; +import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic"; +import { AgentHarness, BACKGROUND_CONTEXT, MemorySessionRepo } from "@earendil-works/pi-agent-core"; +import { inventoryQuery } from "./common.ts"; + +const captured: any[] = []; +const server = http.createServer((req, res) => { + let body = ""; req.on("data", (c) => (body += c)); req.on("end", () => { + captured.push({ method: req.method, url: req.url, auth: req.headers["authorization"] ?? req.headers["x-api-key"], body: JSON.parse(body || "{}") }); + res.writeHead(400, { "content-type": "application/json" }); + res.end(JSON.stringify({ error: { type: "invalid_request_error", message: "capture server: refusing on purpose" } })); + }); +}); +await new Promise((r) => server.listen(0, "127.0.0.1", r)); +const base = `http://127.0.0.1:${(server.address() as any).port}`; + +// Secret comes from our own secret store; resolve() is called at request time. No env var lookup. +const secret = (name: string) => ({ name, resolve: async () => ({ auth: { apiKey: `sk-test-${name}` } }) }); + +const anthropicCompatible: Model<"anthropic-messages"> = { + id: "claude-sonnet-4-5", name: "Anthropic-compatible upstream", api: "anthropic-messages", provider: "upstream-anthropic", + baseUrl: base, reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, maxTokens: 8192, +}; +const openaiCompatible: Model<"openai-completions"> = { + id: "qwen3-235b", name: "OpenAI-compatible upstream", api: "openai-completions", provider: "upstream-openai", + baseUrl: `${base}/v1`, reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 131072, maxTokens: 8192, compat: { supportsDeveloperRole: false, thinkingFormat: "qwen" }, +}; + +const models = createModels(); +models.setProvider(createProvider({ id: "upstream-anthropic", auth: { apiKey: secret("anthropic") }, models: [anthropicCompatible], api: anthropicMessagesApi() })); +models.setProvider(createProvider({ id: "upstream-openai", auth: { apiKey: secret("openai") }, models: [openaiCompatible], api: openAICompletionsApi() })); + +// Built-in provider catalog is available too (no request made): +const builtin = anthropicProvider(); +const cat = createModels(); cat.setProvider(builtin); +console.log("builtin anthropic models (first 5):", cat.getModels("anthropic").slice(0, 5).map((m) => m.id).join(", ")); +console.log("builtin anthropic auth configured without env?", JSON.stringify(await cat.checkAuth("anthropic"))); +console.log("supported thinking levels, custom anthropic model:", getSupportedThinkingLevels(anthropicCompatible).join(",")); +console.log("supported thinking levels, custom openai model:", getSupportedThinkingLevels(openaiCompatible).join(",")); + +for (const model of [anthropicCompatible, openaiCompatible] as Model[]) { + const repo = new MemorySessionRepo(); + const session = await repo.create({}, BACKGROUND_CONTEXT); + const { harness } = await AgentHarness.create({ + session, models, model, systemPrompt: "媒体库助手", tools: [inventoryQuery], toolContext: { actorId: "u", grants: [] }, + thinkingLevel: "high", + compaction: { enabled: true, reserveTokens: 8000, keepRecentTokens: 12000 }, + retry: { enabled: false, maxRetries: 0, baseDelayMs: 0 }, + streamOptions: { timeoutMs: 5000, maxRetries: 0, headers: { "x-umt-run": "spike" } }, + }, BACKGROUND_CONTEXT); + harness.hooks.on("before_request", (ev) => { console.log(`[before_request] ${ev.model.provider}/${ev.model.id} step=${ev.step} attempt=${ev.attempt}`); return undefined; }); + const lane = await harness.lane("main", BACKGROUND_CONTEXT); + const r = await lane.prompt("库里有 Dune 吗", undefined, BACKGROUND_CONTEXT); + console.log(`[${model.provider}] run:`, JSON.stringify(r.ok ? { status: r.value.status, error: (r.value as any).error } : r.error).slice(0, 300)); + await harness.close(BACKGROUND_CONTEXT); +} +for (const c of captured) { + const b = c.body; + console.log(`CAPTURED ${c.method} ${c.url} auth=${c.auth}`); + console.log(` model=${b.model} max_tokens=${b.max_tokens ?? b.max_completion_tokens} thinking=${JSON.stringify(b.thinking ?? b.enable_thinking ?? b.reasoning_effort)} tools=${JSON.stringify((b.tools ?? []).map((t: any) => t.name ?? t.function?.name))} stream=${b.stream}`); +} +server.close();