@desplega.ai/agent-swarm 1.158.0 → 1.159.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
- package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
- package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
- package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
- package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
- package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
- package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
- package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
- package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
- package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
- package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
- package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
- package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
- package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
- package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
- package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
- package/dist/cli-0xqde53b.js +176 -0
- package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
- package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
- package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
- package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
- package/dist/cli-3gvrr8vj.js +1422 -0
- package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
- package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
- package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
- package/dist/cli-503ya656.js +455 -0
- package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
- package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
- package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
- package/dist/cli-7cxw8b60.js +14910 -0
- package/dist/cli-7kyk9jr3.js +22 -0
- package/dist/cli-7zttzetj.js +324 -0
- package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
- package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
- package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
- package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
- package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
- package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
- package/dist/cli-ca2w7zv7.js +115 -0
- package/dist/cli-cpvsaqsw.js +333 -0
- package/dist/cli-cygyqj3b.js +754 -0
- package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
- package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
- package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
- package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
- package/dist/cli-ejpygnqz.js +3815 -0
- package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
- package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
- package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
- package/dist/cli-gzyh0j0g.js +8455 -0
- package/dist/cli-hgqfttfc.js +183 -0
- package/dist/cli-hj65wsts.js +17755 -0
- package/dist/cli-j9z8mfx3.js +476 -0
- package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
- package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
- package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
- package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
- package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
- package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
- package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
- package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
- package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
- package/dist/cli-qvnnx4ct.js +1051 -0
- package/dist/cli-rb9m6rpe.js +4980 -0
- package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
- package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
- package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
- package/dist/cli-rykfmgwg.js +102 -0
- package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
- package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
- package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
- package/dist/cli-wd9hz40d.js +69 -0
- package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
- package/dist/cli-xgpf00sk.js +2149 -0
- package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
- package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
- package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
- package/dist/cli.js +38 -29
- package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
- package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
- package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
- package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
- package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
- package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
- package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
- package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
- package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
- package/dist/execute-71ysgv35.js +965 -0
- package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
- package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
- package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
- package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
- package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
- package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
- package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
- package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
- package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
- package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
- package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
- package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
- package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
- package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
- package/dist/llama-cpp-classify-t2tccams.js +335 -0
- package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
- package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
- package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
- package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
- package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
- package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
- package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
- package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
- package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
- package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
- package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
- package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
- package/dist/pricing-refresh-0v77385v.js +39 -0
- package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
- package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
- package/dist/runtime-ktjysacx.js +24 -0
- package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
- package/dist/typesafe-system-one-96960jxp.js +27 -0
- package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
- package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
- package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
- package/openapi.json +863 -14
- package/package.json +9 -8
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/tasks/human-free.ts +442 -0
- package/src/be/db/tasks/read.ts +56 -1
- package/src/be/db/tasks/write.ts +107 -11
- package/src/be/db.ts +89 -165
- package/src/be/harness-model-support.ts +182 -0
- package/src/be/human-free-drain.ts +67 -0
- package/src/be/migrations/182_task_human_free_flag.sql +63 -0
- package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
- package/src/be/migrations/184_model_catalog.sql +120 -0
- package/src/be/model-catalog-store.ts +644 -0
- package/src/be/model-tier-keys.ts +31 -0
- package/src/be/model-tier-resolution.ts +489 -0
- package/src/be/model-validation.ts +112 -0
- package/src/be/models-catalog.ts +61 -7
- package/src/be/modelsdev-cache.json +82 -0
- package/src/be/modelsdev-cache.ts +2 -0
- package/src/be/pricing-refresh.ts +154 -12
- package/src/be/scripts/typecheck.ts +15 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
- package/src/be/swarm-config-guard.ts +6 -0
- package/src/cli.tsx +10 -0
- package/src/commands/claude-managed-setup.ts +3 -1
- package/src/commands/runner.ts +76 -2
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +64 -15
- package/src/heartbeat/templates.ts +2 -2
- package/src/http/agents.ts +49 -1
- package/src/http/index.ts +8 -0
- package/src/http/models-catalog.ts +357 -8
- package/src/http/poll.ts +45 -2
- package/src/http/schedules.ts +36 -6
- package/src/http/session-data.ts +2 -5
- package/src/http/stats.ts +6 -8
- package/src/http/tasks.ts +26 -1
- package/src/http/usage-cache.ts +56 -15
- package/src/http/workflows.ts +35 -8
- package/src/prompts/session-templates.ts +1 -0
- package/src/providers/claude-managed-models.ts +47 -161
- package/src/providers/codex-adapter.ts +4 -4
- package/src/providers/codex-models.ts +65 -198
- package/src/providers/reasoning-effort.ts +44 -140
- package/src/rbac/legacy-policy.ts +25 -0
- package/src/rbac/permissions.ts +10 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/types/stdlib.d.ts +15 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
- package/src/server.ts +6 -0
- package/src/slack/message-text.ts +56 -2
- package/src/slack/thread-buffer.ts +10 -2
- package/src/tests/agents-list-model-display.test.ts +4 -6
- package/src/tests/base-prompt.test.ts +3 -0
- package/src/tests/claude-adapter.test.ts +10 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -18
- package/src/tests/codex-adapter.test.ts +2 -1
- package/src/tests/codex-gpt6-release.test.ts +66 -5
- package/src/tests/context-window.test.ts +7 -5
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +5 -1
- package/src/tests/db-task-write-contract.test.ts +4 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/explicit-model-validation.test.ts +332 -0
- package/src/tests/harness-model-support.test.ts +374 -0
- package/src/tests/heartbeat-checklist-config.test.ts +71 -0
- package/src/tests/heartbeat-checklist.test.ts +4 -2
- package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
- package/src/tests/human-free-flag.test.ts +279 -0
- package/src/tests/human-free-oracle.ts +57 -0
- package/src/tests/human-free-reclassify.test.ts +669 -0
- package/src/tests/migration-046-budgets.test.ts +3 -3
- package/src/tests/model-catalog-migration.test.ts +134 -0
- package/src/tests/model-catalog-table.test.ts +249 -0
- package/src/tests/model-catalog-write-access.test.ts +387 -0
- package/src/tests/model-tier-resolution.test.ts +519 -0
- package/src/tests/models-catalog.test.ts +1 -1
- package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
- package/src/tests/package-publish.test.ts +137 -36
- package/src/tests/pricing-routes.test.ts +2 -2
- package/src/tests/rbac-engine.test.ts +42 -2
- package/src/tests/reasoning-effort.test.ts +109 -0
- package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
- package/src/tests/runtime-model-catalog.test.ts +86 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/seed-scripts.test.ts +89 -0
- package/src/tests/session-costs.test.ts +14 -11
- package/src/tests/slack-message-text.test.ts +101 -0
- package/src/tests/slack-thread-buffer-context.test.ts +93 -0
- package/src/tests/task-cascade-fail.test.ts +148 -2
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/usage-cache.test.ts +166 -0
- package/src/tests/workflow-executors.test.ts +5 -2
- package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
- package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
- package/src/tests/workflow-system-one-decision.test.ts +2063 -0
- package/src/tools/model-catalog/index.ts +137 -0
- package/src/tools/schedules/create-schedule.ts +16 -1
- package/src/tools/schedules/patch-schedule.ts +17 -0
- package/src/tools/schedules/update-schedule.ts +17 -0
- package/src/tools/send-task.ts +14 -0
- package/src/tools/task-action.ts +12 -0
- package/src/tools/tool-config.ts +3 -1
- package/src/tools/workflows/create-workflow.ts +9 -2
- package/src/tools/workflows/patch-workflow-node.ts +17 -7
- package/src/tools/workflows/patch-workflow.ts +9 -2
- package/src/tools/workflows/update-workflow.ts +9 -2
- package/src/types.ts +30 -1
- package/src/utils/context-window.ts +47 -53
- package/src/utils/harness-cli-version.ts +115 -0
- package/src/utils/harness-model-error.ts +33 -0
- package/src/utils/runtime-model-catalog.ts +137 -0
- package/src/workflows/approval-resolution.ts +39 -0
- package/src/workflows/definition.ts +7 -0
- package/src/workflows/engine.ts +69 -1
- package/src/workflows/executors/agent-task.ts +9 -0
- package/src/workflows/executors/base.ts +53 -0
- package/src/workflows/executors/human-in-the-loop.ts +8 -8
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +1232 -0
- package/src/workflows/executors/system-one-providers.ts +301 -0
- package/src/workflows/executors/workflow-llm.ts +13 -1
- package/src/workflows/readiness.ts +86 -0
- package/src/workflows/recovery.ts +12 -12
- package/src/workflows/resume.ts +23 -9
- package/templates/schedules/daily-blocker-digest/content.md +4 -5
- package/templates/skills/agent-browser/SKILL.md +6 -5
- package/templates/skills/agent-browser/content.md +6 -5
- package/templates/skills/heartbeat-runbook/config.json +1 -1
- package/templates/skills/heartbeat-runbook/content.md +2 -2
- package/templates/skills/swarm-scripts/SKILL.md +1 -1
- package/templates/skills/swarm-scripts/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +81 -1
- package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
- package/templates/workflows/pr-review-status-sweep/content.md +2 -0
- package/dist/claude-sdk-session-t442qz6f.js +0 -39108
- package/dist/cli-0xjkj1j7.js +0 -61
- package/dist/cli-65hjmev5.js +0 -1141
- package/dist/cli-aw1jwp1r.js +0 -14075
- package/dist/cli-h967nb6r.js +0 -6603
- package/dist/cli-vapyk8sw.js +0 -20
- package/dist/cli-ws6rqycm.js +0 -247
- package/dist/pricing-refresh-v1cbr9te.js +0 -27
- package/src/providers/modelsdev-reasoning.json +0 -6453
- package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
- package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
|
@@ -0,0 +1,2149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSwarmEventHandler
|
|
3
|
+
} from "./cli-hsxj0yar.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveCodexPrompt
|
|
6
|
+
} from "./cli-r94z9p1z.js";
|
|
7
|
+
import {
|
|
8
|
+
readPkgVersion
|
|
9
|
+
} from "./cli-2djtr51b.js";
|
|
10
|
+
import {
|
|
11
|
+
buildOtelTraceparentEnv
|
|
12
|
+
} from "./cli-q2j38vre.js";
|
|
13
|
+
import {
|
|
14
|
+
summarizeSession
|
|
15
|
+
} from "./cli-5f2hq128.js";
|
|
16
|
+
import {
|
|
17
|
+
CTX_MODE_NUDGE_EVERY
|
|
18
|
+
} from "./cli-zm7njwzd.js";
|
|
19
|
+
import {
|
|
20
|
+
CONTEXT_FORMULA,
|
|
21
|
+
clampContextPercent,
|
|
22
|
+
computeContextUsedUnified
|
|
23
|
+
} from "./cli-wd9hz40d.js";
|
|
24
|
+
import {
|
|
25
|
+
SessionErrorTracker
|
|
26
|
+
} from "./cli-npssxgaz.js";
|
|
27
|
+
import {
|
|
28
|
+
applyReasoningEffort
|
|
29
|
+
} from "./cli-ca2w7zv7.js";
|
|
30
|
+
import {
|
|
31
|
+
buildRatingsFromLlm,
|
|
32
|
+
fetchRetrievalsForTask,
|
|
33
|
+
getMemoryRaterNames,
|
|
34
|
+
postRatings
|
|
35
|
+
} from "./cli-cpvsaqsw.js";
|
|
36
|
+
import {
|
|
37
|
+
swarmRuntimeInstanceId
|
|
38
|
+
} from "./cli-re8z5p8p.js";
|
|
39
|
+
import {
|
|
40
|
+
runtimeCatalogModel
|
|
41
|
+
} from "./cli-m4excttz.js";
|
|
42
|
+
import {
|
|
43
|
+
detachedProcessGroup,
|
|
44
|
+
registerProcessGroup,
|
|
45
|
+
terminateProcessGroup
|
|
46
|
+
} from "./cli-y89t4nca.js";
|
|
47
|
+
import {
|
|
48
|
+
getApiKey
|
|
49
|
+
} from "./cli-f14fvzag.js";
|
|
50
|
+
import {
|
|
51
|
+
CodexOAuthRefreshError,
|
|
52
|
+
credentialsToAuthJson,
|
|
53
|
+
getValidCodexOAuth
|
|
54
|
+
} from "./cli-vhjwh6cz.js";
|
|
55
|
+
import {
|
|
56
|
+
DEFAULT_MODEL_TIER_MAP,
|
|
57
|
+
init_types
|
|
58
|
+
} from "./cli-e4xnc0qp.js";
|
|
59
|
+
import {
|
|
60
|
+
init_src
|
|
61
|
+
} from "./cli-7zttzetj.js";
|
|
62
|
+
import {
|
|
63
|
+
init_package,
|
|
64
|
+
package_default
|
|
65
|
+
} from "./cli-48527fpw.js";
|
|
66
|
+
import {
|
|
67
|
+
init_secret_scrubber,
|
|
68
|
+
scrubSecrets
|
|
69
|
+
} from "./cli-r2ap2czm.js";
|
|
70
|
+
import {
|
|
71
|
+
__require
|
|
72
|
+
} from "./cli-bc05qbss.js";
|
|
73
|
+
|
|
74
|
+
// src/providers/codex-adapter.ts
|
|
75
|
+
import { existsSync as nodeExistsSync } from "node:fs";
|
|
76
|
+
import { join as join2 } from "node:path";
|
|
77
|
+
init_secret_scrubber();
|
|
78
|
+
|
|
79
|
+
// src/providers/codex-agents-md.ts
|
|
80
|
+
import { lstat, mkdir, readlink, realpath, symlink, unlink } from "node:fs/promises";
|
|
81
|
+
import { homedir } from "node:os";
|
|
82
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
83
|
+
var BLOCK_OPEN = "<swarm_system_prompt>";
|
|
84
|
+
var BLOCK_CLOSE = "</swarm_system_prompt>";
|
|
85
|
+
var BLOCK_REGEX = /<swarm_system_prompt>[\s\S]*?<\/swarm_system_prompt>\n?/;
|
|
86
|
+
var DEFAULT_STATE_DIR = join(homedir(), ".cache", "agent-swarm", "codex-agents-md");
|
|
87
|
+
var NOOP_HANDLE = {
|
|
88
|
+
cleanup: async () => {}
|
|
89
|
+
};
|
|
90
|
+
async function readIfExists(path) {
|
|
91
|
+
const file = Bun.file(path);
|
|
92
|
+
return await file.exists() ? await file.text() : null;
|
|
93
|
+
}
|
|
94
|
+
function recordPathFor(stateDir, realCwd) {
|
|
95
|
+
const key = new Bun.CryptoHasher("sha256").update(realCwd).digest("hex");
|
|
96
|
+
return join(stateDir, `${key}.json`);
|
|
97
|
+
}
|
|
98
|
+
async function readRecord(recordPath, realCwd) {
|
|
99
|
+
try {
|
|
100
|
+
const record = JSON.parse(await Bun.file(recordPath).text());
|
|
101
|
+
if (record.cwd === realCwd && typeof record.linkTarget === "string") {
|
|
102
|
+
return { cwd: record.cwd, linkTarget: record.linkTarget };
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
async function inTreeTarget(realCwd, agentsMdPath, linkTarget) {
|
|
108
|
+
const target = await realpath(resolve(dirname(agentsMdPath), linkTarget)).catch(() => null);
|
|
109
|
+
const prefix = realCwd.endsWith(sep) ? realCwd : `${realCwd}${sep}`;
|
|
110
|
+
if (target === null || !target.startsWith(prefix)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const stat = await lstat(target).catch(() => null);
|
|
114
|
+
return stat?.isFile() ? target : null;
|
|
115
|
+
}
|
|
116
|
+
async function restoreSymlink(agentsMdPath, realCwd, linkTarget) {
|
|
117
|
+
const stat = await lstat(agentsMdPath).catch(() => null);
|
|
118
|
+
const current = stat?.isFile() ? await Bun.file(agentsMdPath).text() : null;
|
|
119
|
+
await unlink(agentsMdPath).catch(() => {});
|
|
120
|
+
await symlink(linkTarget, agentsMdPath);
|
|
121
|
+
if (current === null) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const target = await inTreeTarget(realCwd, agentsMdPath, linkTarget);
|
|
125
|
+
if (target === null) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const stripped = current.replace(BLOCK_REGEX, "");
|
|
129
|
+
if (stripped !== await Bun.file(target).text()) {
|
|
130
|
+
await Bun.write(target, stripped);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function writeCodexAgentsMd(cwd, systemPrompt, options = {}) {
|
|
134
|
+
if (!cwd || !systemPrompt) {
|
|
135
|
+
return NOOP_HANDLE;
|
|
136
|
+
}
|
|
137
|
+
const agentsMdPath = join(cwd, "AGENTS.md");
|
|
138
|
+
const claudeMdPath = join(cwd, "CLAUDE.md");
|
|
139
|
+
const stateDir = options.stateDir ?? DEFAULT_STATE_DIR;
|
|
140
|
+
const realCwd = await realpath(cwd).catch(() => resolve(cwd));
|
|
141
|
+
const recordPath = recordPathFor(stateDir, realCwd);
|
|
142
|
+
const block = `${BLOCK_OPEN}
|
|
143
|
+
${systemPrompt}
|
|
144
|
+
${BLOCK_CLOSE}`;
|
|
145
|
+
let stat = await lstat(agentsMdPath).catch(() => null);
|
|
146
|
+
const record = await readRecord(recordPath, realCwd);
|
|
147
|
+
if (record) {
|
|
148
|
+
if (stat?.isFile() && BLOCK_REGEX.test(await Bun.file(agentsMdPath).text())) {
|
|
149
|
+
await restoreSymlink(agentsMdPath, realCwd, record.linkTarget);
|
|
150
|
+
stat = await lstat(agentsMdPath);
|
|
151
|
+
}
|
|
152
|
+
await unlink(recordPath).catch(() => {});
|
|
153
|
+
}
|
|
154
|
+
if (stat?.isSymbolicLink()) {
|
|
155
|
+
const linkTarget = await readlink(agentsMdPath);
|
|
156
|
+
const target = await inTreeTarget(realCwd, agentsMdPath, linkTarget);
|
|
157
|
+
const targetContent = target !== null ? await Bun.file(target).text() : "";
|
|
158
|
+
await mkdir(stateDir, { recursive: true });
|
|
159
|
+
await Bun.write(recordPath, JSON.stringify({ cwd: realCwd, linkTarget }));
|
|
160
|
+
await unlink(agentsMdPath);
|
|
161
|
+
await Bun.write(agentsMdPath, `${block}
|
|
162
|
+
${targetContent.replace(BLOCK_REGEX, "")}`);
|
|
163
|
+
return {
|
|
164
|
+
async cleanup() {
|
|
165
|
+
try {
|
|
166
|
+
await restoreSymlink(agentsMdPath, realCwd, linkTarget);
|
|
167
|
+
await unlink(recordPath).catch(() => {});
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
let createdFresh = false;
|
|
173
|
+
let newContent;
|
|
174
|
+
if (!stat) {
|
|
175
|
+
const claudeContent = await readIfExists(claudeMdPath);
|
|
176
|
+
newContent = claudeContent !== null ? `${block}
|
|
177
|
+
|
|
178
|
+
${claudeContent}` : `${block}
|
|
179
|
+
`;
|
|
180
|
+
createdFresh = true;
|
|
181
|
+
} else {
|
|
182
|
+
const existingContent = await Bun.file(agentsMdPath).text();
|
|
183
|
+
if (BLOCK_REGEX.test(existingContent)) {
|
|
184
|
+
newContent = existingContent.replace(BLOCK_REGEX, `${block}
|
|
185
|
+
`);
|
|
186
|
+
} else {
|
|
187
|
+
newContent = `${block}
|
|
188
|
+
${existingContent}`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
await Bun.write(agentsMdPath, newContent);
|
|
192
|
+
return {
|
|
193
|
+
async cleanup() {
|
|
194
|
+
try {
|
|
195
|
+
if (createdFresh) {
|
|
196
|
+
await Bun.$`rm -f ${agentsMdPath}`.quiet().nothrow();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const currentFile = Bun.file(agentsMdPath);
|
|
200
|
+
if (!await currentFile.exists()) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const currentContent = await currentFile.text();
|
|
204
|
+
const stripped = currentContent.replace(BLOCK_REGEX, "");
|
|
205
|
+
await Bun.write(agentsMdPath, stripped);
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/providers/codex-app-server.ts
|
|
212
|
+
init_package();
|
|
213
|
+
init_secret_scrubber();
|
|
214
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
215
|
+
var CLOSE_GRACE_MS = 250;
|
|
216
|
+
var EXIT_STREAM_DRAIN_MS = 100;
|
|
217
|
+
var MAX_STDOUT_LINE_LENGTH = 16 * 1024 * 1024;
|
|
218
|
+
var MAX_STDERR_LINE_LENGTH = 64 * 1024;
|
|
219
|
+
var MAX_STDERR_TAIL_LENGTH = 16 * 1024;
|
|
220
|
+
|
|
221
|
+
class CodexAppServerRpcError extends Error {
|
|
222
|
+
code;
|
|
223
|
+
data;
|
|
224
|
+
constructor(method, error) {
|
|
225
|
+
super(`Codex app-server ${method} failed (${error.code}): ${error.message}`);
|
|
226
|
+
this.name = "CodexAppServerRpcError";
|
|
227
|
+
this.code = error.code;
|
|
228
|
+
this.data = error.data;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
class CodexAppServer {
|
|
233
|
+
proc;
|
|
234
|
+
pending = new Map;
|
|
235
|
+
notificationListeners = new Set;
|
|
236
|
+
closeListeners = new Set;
|
|
237
|
+
requestTimeoutMs;
|
|
238
|
+
onStderr;
|
|
239
|
+
ready;
|
|
240
|
+
stderrDone;
|
|
241
|
+
nextId = 0;
|
|
242
|
+
stderrTail = "";
|
|
243
|
+
initializedVersion;
|
|
244
|
+
terminalError;
|
|
245
|
+
closePromise;
|
|
246
|
+
constructor(options) {
|
|
247
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
248
|
+
if (!Number.isFinite(this.requestTimeoutMs) || this.requestTimeoutMs <= 0) {
|
|
249
|
+
throw new Error("Codex app-server requestTimeoutMs must be a positive number");
|
|
250
|
+
}
|
|
251
|
+
this.onStderr = options.onStderr;
|
|
252
|
+
const codexPath = options.codexPath ?? resolveCodexPath(options.env.PATH);
|
|
253
|
+
const command = [codexPath, "app-server", "--listen", "stdio://"];
|
|
254
|
+
const env = Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== undefined));
|
|
255
|
+
this.proc = Bun.spawn(command, {
|
|
256
|
+
env,
|
|
257
|
+
stdin: "pipe",
|
|
258
|
+
stdout: "pipe",
|
|
259
|
+
stderr: "pipe"
|
|
260
|
+
});
|
|
261
|
+
this.consumeStdout().catch((error) => this.fail(asError(error)));
|
|
262
|
+
this.stderrDone = this.consumeStderr().catch((error) => {
|
|
263
|
+
this.fail(asError(error));
|
|
264
|
+
});
|
|
265
|
+
this.proc.exited.then(async (exitCode) => {
|
|
266
|
+
await Promise.race([this.stderrDone, Bun.sleep(EXIT_STREAM_DRAIN_MS)]);
|
|
267
|
+
if (this.closePromise)
|
|
268
|
+
return;
|
|
269
|
+
const suffix = this.stderrTail ? `: ${this.stderrTail}` : "";
|
|
270
|
+
this.fail(new Error(`Codex app-server exited with code ${exitCode}${suffix}`));
|
|
271
|
+
});
|
|
272
|
+
this.ready = Promise.resolve().then(() => this.initialize());
|
|
273
|
+
this.ready.catch(() => {});
|
|
274
|
+
}
|
|
275
|
+
async request(method, params) {
|
|
276
|
+
await this.ready;
|
|
277
|
+
return this.requestRaw(method, params);
|
|
278
|
+
}
|
|
279
|
+
get version() {
|
|
280
|
+
return this.initializedVersion;
|
|
281
|
+
}
|
|
282
|
+
onNotification(listener) {
|
|
283
|
+
this.notificationListeners.add(listener);
|
|
284
|
+
return () => this.notificationListeners.delete(listener);
|
|
285
|
+
}
|
|
286
|
+
onClose(listener) {
|
|
287
|
+
if (this.terminalError) {
|
|
288
|
+
listener(this.terminalError);
|
|
289
|
+
return () => {};
|
|
290
|
+
}
|
|
291
|
+
this.closeListeners.add(listener);
|
|
292
|
+
return () => this.closeListeners.delete(listener);
|
|
293
|
+
}
|
|
294
|
+
close() {
|
|
295
|
+
if (this.closePromise)
|
|
296
|
+
return this.closePromise;
|
|
297
|
+
this.rejectPending(new Error("Codex app-server closed"));
|
|
298
|
+
this.closePromise = this.terminate();
|
|
299
|
+
return this.closePromise;
|
|
300
|
+
}
|
|
301
|
+
async initialize() {
|
|
302
|
+
const response = await this.requestRaw("initialize", {
|
|
303
|
+
clientInfo: {
|
|
304
|
+
name: "agent-swarm",
|
|
305
|
+
title: "Agent Swarm",
|
|
306
|
+
version: package_default.version
|
|
307
|
+
},
|
|
308
|
+
capabilities: {
|
|
309
|
+
experimentalApi: false,
|
|
310
|
+
requestAttestation: false
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
if (typeof response.userAgent === "string") {
|
|
314
|
+
this.initializedVersion = parseVersion(response.userAgent);
|
|
315
|
+
}
|
|
316
|
+
this.write({ method: "initialized" });
|
|
317
|
+
}
|
|
318
|
+
requestRaw(method, params) {
|
|
319
|
+
if (this.terminalError)
|
|
320
|
+
return Promise.reject(this.terminalError);
|
|
321
|
+
if (this.closePromise)
|
|
322
|
+
return Promise.reject(new Error("Codex app-server closed"));
|
|
323
|
+
const id = this.nextId++;
|
|
324
|
+
return new Promise((resolve2, reject) => {
|
|
325
|
+
const timer = setTimeout(() => {
|
|
326
|
+
this.pending.delete(id);
|
|
327
|
+
reject(new Error(`Codex app-server ${method} timed out after ${this.requestTimeoutMs}ms`));
|
|
328
|
+
}, this.requestTimeoutMs);
|
|
329
|
+
this.pending.set(id, {
|
|
330
|
+
method,
|
|
331
|
+
resolve: resolve2,
|
|
332
|
+
reject,
|
|
333
|
+
timer
|
|
334
|
+
});
|
|
335
|
+
try {
|
|
336
|
+
this.write(params === undefined ? { method, id } : { method, id, params });
|
|
337
|
+
} catch (error) {
|
|
338
|
+
clearTimeout(timer);
|
|
339
|
+
this.pending.delete(id);
|
|
340
|
+
reject(asError(error));
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
write(message) {
|
|
345
|
+
if (this.terminalError)
|
|
346
|
+
throw this.terminalError;
|
|
347
|
+
if (this.closePromise)
|
|
348
|
+
throw new Error("Codex app-server closed");
|
|
349
|
+
const stdin = this.proc.stdin;
|
|
350
|
+
stdin.write(`${JSON.stringify(message)}
|
|
351
|
+
`);
|
|
352
|
+
stdin.flush();
|
|
353
|
+
}
|
|
354
|
+
async consumeStdout() {
|
|
355
|
+
await readLines(this.proc.stdout, MAX_STDOUT_LINE_LENGTH, (line) => this.handleLine(line));
|
|
356
|
+
}
|
|
357
|
+
async consumeStderr() {
|
|
358
|
+
await readLines(this.proc.stderr, MAX_STDERR_LINE_LENGTH, (line) => {
|
|
359
|
+
const scrubbed = scrubSecrets(line);
|
|
360
|
+
this.stderrTail = `${this.stderrTail}${this.stderrTail ? `
|
|
361
|
+
` : ""}${scrubbed}`.slice(-MAX_STDERR_TAIL_LENGTH);
|
|
362
|
+
if (!this.onStderr)
|
|
363
|
+
return;
|
|
364
|
+
try {
|
|
365
|
+
this.onStderr(scrubbed);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.error(`[codex-app-server] stderr listener failed: ${scrubSecrets(asError(error).message)}`);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
handleLine(line) {
|
|
372
|
+
let message;
|
|
373
|
+
try {
|
|
374
|
+
message = JSON.parse(line);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new Error("Codex app-server emitted malformed JSON", {
|
|
377
|
+
cause: error
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (!isRecord(message)) {
|
|
381
|
+
throw new Error("Codex app-server emitted a non-object JSON message");
|
|
382
|
+
}
|
|
383
|
+
if (isRequestId(message.id)) {
|
|
384
|
+
if (typeof message.method === "string") {
|
|
385
|
+
this.rejectServerRequest(message.id, message.method);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.handleResponse(message.id, message);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (typeof message.method === "string") {
|
|
392
|
+
const notification = {
|
|
393
|
+
method: message.method,
|
|
394
|
+
...Object.hasOwn(message, "params") ? { params: message.params } : {},
|
|
395
|
+
...typeof message.emittedAtMs === "number" ? { emittedAtMs: message.emittedAtMs } : {}
|
|
396
|
+
};
|
|
397
|
+
for (const listener of this.notificationListeners) {
|
|
398
|
+
try {
|
|
399
|
+
listener(notification);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.error(`[codex-app-server] notification listener failed: ${scrubSecrets(asError(error).message)}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
throw new Error("Codex app-server emitted an invalid protocol message");
|
|
407
|
+
}
|
|
408
|
+
handleResponse(id, message) {
|
|
409
|
+
const pending = this.pending.get(id);
|
|
410
|
+
if (!pending)
|
|
411
|
+
return;
|
|
412
|
+
this.pending.delete(id);
|
|
413
|
+
clearTimeout(pending.timer);
|
|
414
|
+
if (isRpcError(message.error)) {
|
|
415
|
+
pending.reject(new CodexAppServerRpcError(pending.method, message.error));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (Object.hasOwn(message, "result")) {
|
|
419
|
+
pending.resolve(message.result);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
pending.reject(new Error(`Codex app-server returned an invalid response for ${pending.method}`));
|
|
423
|
+
}
|
|
424
|
+
rejectServerRequest(id, method) {
|
|
425
|
+
this.write({
|
|
426
|
+
id,
|
|
427
|
+
error: {
|
|
428
|
+
code: -32601,
|
|
429
|
+
message: `Client does not support server request: ${method}`
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
fail(error) {
|
|
434
|
+
if (this.terminalError || this.closePromise)
|
|
435
|
+
return;
|
|
436
|
+
this.terminalError = error;
|
|
437
|
+
this.rejectPending(error);
|
|
438
|
+
this.closePromise = this.terminate();
|
|
439
|
+
for (const listener of this.closeListeners) {
|
|
440
|
+
try {
|
|
441
|
+
listener(error);
|
|
442
|
+
} catch (listenerError) {
|
|
443
|
+
console.error(`[codex-app-server] close listener failed: ${scrubSecrets(asError(listenerError).message)}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
this.closeListeners.clear();
|
|
447
|
+
}
|
|
448
|
+
rejectPending(error) {
|
|
449
|
+
for (const pending of this.pending.values()) {
|
|
450
|
+
clearTimeout(pending.timer);
|
|
451
|
+
pending.reject(error);
|
|
452
|
+
}
|
|
453
|
+
this.pending.clear();
|
|
454
|
+
}
|
|
455
|
+
async terminate() {
|
|
456
|
+
try {
|
|
457
|
+
this.proc.stdin.end();
|
|
458
|
+
} catch {}
|
|
459
|
+
try {
|
|
460
|
+
this.proc.kill("SIGTERM");
|
|
461
|
+
} catch {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const exited = this.proc.exited.then(() => {
|
|
465
|
+
return;
|
|
466
|
+
});
|
|
467
|
+
await Promise.race([exited, Bun.sleep(CLOSE_GRACE_MS)]);
|
|
468
|
+
if (this.proc.exitCode === null) {
|
|
469
|
+
try {
|
|
470
|
+
this.proc.kill("SIGKILL");
|
|
471
|
+
} catch {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
await Promise.race([exited, Bun.sleep(CLOSE_GRACE_MS)]);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function readLines(stream, maxLineLength, onLine) {
|
|
479
|
+
const reader = stream.getReader();
|
|
480
|
+
const decoder = new TextDecoder;
|
|
481
|
+
let buffered = "";
|
|
482
|
+
try {
|
|
483
|
+
while (true) {
|
|
484
|
+
const { done, value } = await reader.read();
|
|
485
|
+
buffered += decoder.decode(value, { stream: !done });
|
|
486
|
+
let newline = buffered.indexOf(`
|
|
487
|
+
`);
|
|
488
|
+
while (newline !== -1) {
|
|
489
|
+
const line = buffered.slice(0, newline).replace(/\r$/, "");
|
|
490
|
+
buffered = buffered.slice(newline + 1);
|
|
491
|
+
if (line.length > maxLineLength) {
|
|
492
|
+
throw new Error(`Codex app-server output line exceeded ${maxLineLength} characters`);
|
|
493
|
+
}
|
|
494
|
+
if (line)
|
|
495
|
+
onLine(line);
|
|
496
|
+
newline = buffered.indexOf(`
|
|
497
|
+
`);
|
|
498
|
+
}
|
|
499
|
+
if (buffered.length > maxLineLength) {
|
|
500
|
+
throw new Error(`Codex app-server output line exceeded ${maxLineLength} characters`);
|
|
501
|
+
}
|
|
502
|
+
if (done)
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
if (buffered) {
|
|
506
|
+
const line = buffered.replace(/\r$/, "");
|
|
507
|
+
if (line.length > maxLineLength) {
|
|
508
|
+
throw new Error(`Codex app-server output line exceeded ${maxLineLength} characters`);
|
|
509
|
+
}
|
|
510
|
+
onLine(line);
|
|
511
|
+
}
|
|
512
|
+
} finally {
|
|
513
|
+
reader.releaseLock();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function resolveCodexPath(path) {
|
|
517
|
+
const fromPath = Bun.which("codex", { PATH: path ?? process.env.PATH });
|
|
518
|
+
if (fromPath)
|
|
519
|
+
return fromPath;
|
|
520
|
+
try {
|
|
521
|
+
return Bun.resolveSync("@openai/codex/bin/codex.js", import.meta.dir);
|
|
522
|
+
} catch {
|
|
523
|
+
return "codex";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function parseVersion(userAgent) {
|
|
527
|
+
return userAgent.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/)?.[0];
|
|
528
|
+
}
|
|
529
|
+
function isRecord(value) {
|
|
530
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
531
|
+
}
|
|
532
|
+
function isRequestId(value) {
|
|
533
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
534
|
+
}
|
|
535
|
+
function isRpcError(value) {
|
|
536
|
+
return isRecord(value) && typeof value.code === "number" && Number.isFinite(value.code) && typeof value.message === "string";
|
|
537
|
+
}
|
|
538
|
+
function asError(value) {
|
|
539
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/providers/codex-models.ts
|
|
543
|
+
init_src();
|
|
544
|
+
init_types();
|
|
545
|
+
var CODEX_DEFAULT_MODEL = DEFAULT_MODEL_TIER_MAP.codex.regular;
|
|
546
|
+
var CLAUDE_SHORTNAME_TIER = {
|
|
547
|
+
fable: "ultra",
|
|
548
|
+
opus: "smart",
|
|
549
|
+
sonnet: "regular",
|
|
550
|
+
haiku: "smol"
|
|
551
|
+
};
|
|
552
|
+
function resolveCodexModel(modelStr) {
|
|
553
|
+
if (!modelStr)
|
|
554
|
+
return CODEX_DEFAULT_MODEL;
|
|
555
|
+
const normalized = modelStr.toLowerCase();
|
|
556
|
+
const tier = CLAUDE_SHORTNAME_TIER[normalized];
|
|
557
|
+
return tier ? DEFAULT_MODEL_TIER_MAP.codex[tier] : normalized;
|
|
558
|
+
}
|
|
559
|
+
var UNKNOWN_CONTEXT_WINDOW = 200000;
|
|
560
|
+
function getCodexContextWindow(model) {
|
|
561
|
+
const context = runtimeCatalogModel("openai", model)?.limit?.context;
|
|
562
|
+
return typeof context === "number" && context > 0 ? context : UNKNOWN_CONTEXT_WINDOW;
|
|
563
|
+
}
|
|
564
|
+
function getCodexModelPricing(model) {
|
|
565
|
+
const cost = runtimeCatalogModel("openai", model)?.cost;
|
|
566
|
+
if (typeof cost?.input !== "number" || typeof cost.output !== "number")
|
|
567
|
+
return;
|
|
568
|
+
return {
|
|
569
|
+
inputPerMillion: cost.input,
|
|
570
|
+
cachedInputPerMillion: typeof cost.cache_read === "number" ? cost.cache_read : cost.input / 10,
|
|
571
|
+
outputPerMillion: cost.output
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
var _warnedUnknownCodexModels = new Set;
|
|
575
|
+
function computeCodexCostUsd(model, inputTokens, cachedInputTokens, outputTokens) {
|
|
576
|
+
const pricing = getCodexModelPricing(model);
|
|
577
|
+
if (!pricing) {
|
|
578
|
+
if (!_warnedUnknownCodexModels.has(model)) {
|
|
579
|
+
_warnedUnknownCodexModels.add(model);
|
|
580
|
+
console.warn(`[codex] unpriced model ${JSON.stringify(model)} — adapter cost will report $0; ` + "add it to the model catalog (refresh or overlay row). Server-side recompute will tag costSource='unpriced' if the pricing table has no rows.");
|
|
581
|
+
}
|
|
582
|
+
return 0;
|
|
583
|
+
}
|
|
584
|
+
const uncachedInput = Math.max(0, inputTokens - cachedInputTokens);
|
|
585
|
+
const inputCost = uncachedInput / 1e6 * pricing.inputPerMillion;
|
|
586
|
+
const cachedCost = cachedInputTokens / 1e6 * pricing.cachedInputPerMillion;
|
|
587
|
+
const outputCost = outputTokens / 1e6 * pricing.outputPerMillion;
|
|
588
|
+
return inputCost + cachedCost + outputCost;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/providers/codex-oauth/home.ts
|
|
592
|
+
import os from "node:os";
|
|
593
|
+
function codexUserHome() {
|
|
594
|
+
return process.env.HOME || os.homedir();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/providers/codex-swarm-events.ts
|
|
598
|
+
function createCodexSwarmEventHandler(opts) {
|
|
599
|
+
return createSwarmEventHandler({
|
|
600
|
+
...opts,
|
|
601
|
+
sessionIdFallbackPrefix: "codex"
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/providers/codex-adapter.ts
|
|
606
|
+
function checkCodexCredentials(env, opts = {}) {
|
|
607
|
+
const homeDir = opts.homeDir ?? env.HOME ?? "/root";
|
|
608
|
+
const existsSync = opts.fs?.existsSync ?? nodeExistsSync;
|
|
609
|
+
const authFile = `${homeDir}/.codex/auth.json`;
|
|
610
|
+
if (existsSync(authFile)) {
|
|
611
|
+
return { ready: true, missing: [], satisfiedBy: "file" };
|
|
612
|
+
}
|
|
613
|
+
if (env.OPENAI_API_KEY || env.CODEX_OAUTH) {
|
|
614
|
+
return {
|
|
615
|
+
ready: true,
|
|
616
|
+
missing: [],
|
|
617
|
+
satisfiedBy: "side-effect-pending",
|
|
618
|
+
hint: "Credential present in env; entrypoint will materialise ~/.codex/auth.json on next boot."
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (Object.keys(env).some((k) => /^codex_oauth_\d+$/.test(k))) {
|
|
622
|
+
return {
|
|
623
|
+
ready: true,
|
|
624
|
+
missing: [],
|
|
625
|
+
satisfiedBy: "side-effect-pending",
|
|
626
|
+
hint: "Codex OAuth credential pool configured; runner will materialise auth.json per-task."
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
ready: false,
|
|
631
|
+
missing: ["OPENAI_API_KEY", "CODEX_OAUTH", authFile],
|
|
632
|
+
hint: "Set OPENAI_API_KEY (entrypoint runs `codex login --with-api-key`), or store CODEX_OAUTH in swarm_config, or place a pre-authenticated `~/.codex/auth.json` in the worker home."
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
async function resolveCodexAuthMode(config, emit, deps = {}) {
|
|
636
|
+
const fsModule = await import("node:fs/promises");
|
|
637
|
+
const homedir2 = deps.homedir ?? codexUserHome;
|
|
638
|
+
const fs = deps.fs ?? {
|
|
639
|
+
readFile: (path, encoding) => fsModule.readFile(path, encoding),
|
|
640
|
+
mkdir: (path, opts) => fsModule.mkdir(path, opts),
|
|
641
|
+
writeFile: (path, data, opts) => fsModule.writeFile(path, data, opts)
|
|
642
|
+
};
|
|
643
|
+
const authJsonPath = join2(homedir2(), ".codex", "auth.json");
|
|
644
|
+
const readAuthMode = async () => {
|
|
645
|
+
try {
|
|
646
|
+
const raw = await fs.readFile(authJsonPath, "utf-8");
|
|
647
|
+
const parsed = JSON.parse(raw);
|
|
648
|
+
return typeof parsed.auth_mode === "string" ? parsed.auth_mode : null;
|
|
649
|
+
} catch {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
let currentMode = await readAuthMode();
|
|
654
|
+
const slot = config.codexSlot ?? 0;
|
|
655
|
+
const isPoolSlot = config.codexSlot !== undefined;
|
|
656
|
+
if (config.apiUrl && config.apiKey && (currentMode !== "chatgpt" || isPoolSlot)) {
|
|
657
|
+
let oauthCreds = null;
|
|
658
|
+
try {
|
|
659
|
+
oauthCreds = await getValidCodexOAuth(config.apiUrl, config.apiKey, slot);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
if (isPoolSlot) {
|
|
662
|
+
throw new Error(buildPoolRevalidationFailureReason(err, slot));
|
|
663
|
+
}
|
|
664
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
665
|
+
emit({
|
|
666
|
+
type: "raw_stderr",
|
|
667
|
+
content: `[codex] OAuth revalidation failed (non-fatal, non-pool auth): ${message}
|
|
668
|
+
`
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (!oauthCreds && isPoolSlot) {
|
|
672
|
+
throw new Error(`[auth-error] Codex pool slot ${slot} revalidation failed: no credentials found in config store — the slot may have just been quarantined; re-run codex-login for this slot if this persists.`);
|
|
673
|
+
}
|
|
674
|
+
if (oauthCreds) {
|
|
675
|
+
try {
|
|
676
|
+
const authJson = credentialsToAuthJson(oauthCreds, {
|
|
677
|
+
includeRefreshToken: !isPoolSlot
|
|
678
|
+
});
|
|
679
|
+
await fs.mkdir(join2(homedir2(), ".codex"), { recursive: true, mode: 448 });
|
|
680
|
+
await fs.writeFile(authJsonPath, JSON.stringify(authJson, null, 2), { mode: 384 });
|
|
681
|
+
const verb = currentMode === null ? "Restored" : currentMode === "chatgpt" ? "Revalidated" : "Upgraded api-key auth.json to";
|
|
682
|
+
emit({
|
|
683
|
+
type: "raw_stderr",
|
|
684
|
+
content: `[codex] ${verb} OAuth credentials from config store
|
|
685
|
+
`
|
|
686
|
+
});
|
|
687
|
+
currentMode = "chatgpt";
|
|
688
|
+
} catch (err) {
|
|
689
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
690
|
+
emit({
|
|
691
|
+
type: "raw_stderr",
|
|
692
|
+
content: `[codex] Failed to write auth.json: ${message}
|
|
693
|
+
`
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return currentMode;
|
|
699
|
+
}
|
|
700
|
+
function buildPoolRevalidationFailureReason(err, slot) {
|
|
701
|
+
if (err instanceof CodexOAuthRefreshError) {
|
|
702
|
+
if (err.reason === "lock_timeout") {
|
|
703
|
+
return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: timed out waiting for the refresh lock — transient, will retry on next task.`;
|
|
704
|
+
}
|
|
705
|
+
return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: refresh rejected (${err.status ?? "unknown status"} ${err.body ?? ""}) — credential likely revoked; re-run codex-login for this slot.`;
|
|
706
|
+
}
|
|
707
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
708
|
+
return `[auth-error] Codex pool slot ${slot} revalidation failed: ${message}`;
|
|
709
|
+
}
|
|
710
|
+
async function buildCodexConfig(config, model, emit) {
|
|
711
|
+
const mcpServers = {};
|
|
712
|
+
const runtimeInstanceId = swarmRuntimeInstanceId();
|
|
713
|
+
mcpServers["agent-swarm"] = {
|
|
714
|
+
url: `${config.apiUrl}/mcp`,
|
|
715
|
+
http_headers: {
|
|
716
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
717
|
+
"X-Agent-ID": config.agentId,
|
|
718
|
+
"X-Source-Task-Id": config.taskId ?? "",
|
|
719
|
+
...runtimeInstanceId ? { "X-Runtime-Instance-ID": runtimeInstanceId } : {}
|
|
720
|
+
},
|
|
721
|
+
enabled: true,
|
|
722
|
+
startup_timeout_sec: 30,
|
|
723
|
+
tool_timeout_sec: 120
|
|
724
|
+
};
|
|
725
|
+
if (config.apiUrl && config.apiKey && config.agentId) {
|
|
726
|
+
try {
|
|
727
|
+
const res = await fetch(`${config.apiUrl}/api/agents/${config.agentId}/mcp-servers?resolveSecrets=true`, {
|
|
728
|
+
headers: {
|
|
729
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
730
|
+
"X-Agent-ID": config.agentId
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
if (res.ok) {
|
|
734
|
+
const data = await res.json();
|
|
735
|
+
for (const srv of data.servers ?? []) {
|
|
736
|
+
if (!srv.isActive || !srv.isEnabled)
|
|
737
|
+
continue;
|
|
738
|
+
if (srv.transport === "stdio") {
|
|
739
|
+
if (!srv.command)
|
|
740
|
+
continue;
|
|
741
|
+
let parsedArgs = [];
|
|
742
|
+
try {
|
|
743
|
+
parsedArgs = srv.args ? JSON.parse(srv.args) : [];
|
|
744
|
+
} catch {}
|
|
745
|
+
mcpServers[srv.name] = {
|
|
746
|
+
command: srv.command,
|
|
747
|
+
args: parsedArgs,
|
|
748
|
+
env: srv.resolvedEnv ?? {},
|
|
749
|
+
enabled: true,
|
|
750
|
+
startup_timeout_sec: 30,
|
|
751
|
+
tool_timeout_sec: 120
|
|
752
|
+
};
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (srv.transport === "http") {
|
|
756
|
+
if (!srv.url)
|
|
757
|
+
continue;
|
|
758
|
+
let parsedHeaders = {};
|
|
759
|
+
try {
|
|
760
|
+
parsedHeaders = srv.headers ? JSON.parse(srv.headers) : {};
|
|
761
|
+
} catch {}
|
|
762
|
+
mcpServers[srv.name] = {
|
|
763
|
+
url: srv.url,
|
|
764
|
+
http_headers: { ...parsedHeaders, ...srv.resolvedHeaders ?? {} },
|
|
765
|
+
enabled: true,
|
|
766
|
+
startup_timeout_sec: 30,
|
|
767
|
+
tool_timeout_sec: 120
|
|
768
|
+
};
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (srv.transport === "sse") {
|
|
772
|
+
emit({
|
|
773
|
+
type: "raw_stderr",
|
|
774
|
+
content: `[codex] Skipping MCP server "${srv.name}": SSE transport is not yet supported by Codex (tracked in openai/codex#2129).
|
|
775
|
+
`
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
emit({
|
|
781
|
+
type: "raw_stderr",
|
|
782
|
+
content: `[codex] Failed to fetch installed MCP servers: HTTP ${res.status}. Continuing with only the swarm MCP server.
|
|
783
|
+
`
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
} catch (err) {
|
|
787
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
788
|
+
emit({
|
|
789
|
+
type: "raw_stderr",
|
|
790
|
+
content: `[codex] Failed to fetch installed MCP servers: ${message}. Continuing with only the swarm MCP server.
|
|
791
|
+
`
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (process.env.CONTEXT_MODE_DISABLED !== "true") {
|
|
796
|
+
mcpServers["context-mode"] = {
|
|
797
|
+
command: "context-mode",
|
|
798
|
+
enabled: true,
|
|
799
|
+
startup_timeout_sec: 30,
|
|
800
|
+
tool_timeout_sec: 120
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const reasoningApplication = applyReasoningEffort("codex", model, config.reasoningEffort);
|
|
804
|
+
const reasoningConfig = reasoningApplication.kind === "codex-config" ? reasoningApplication.config : {};
|
|
805
|
+
return {
|
|
806
|
+
model,
|
|
807
|
+
approval_policy: "never",
|
|
808
|
+
sandbox_mode: "danger-full-access",
|
|
809
|
+
skip_git_repo_check: true,
|
|
810
|
+
show_raw_agent_reasoning: false,
|
|
811
|
+
features: { apps: false, hooks: true, plugin_hooks: true },
|
|
812
|
+
mcp_servers: mcpServers,
|
|
813
|
+
...reasoningConfig
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function textInput(text) {
|
|
817
|
+
return [{ type: "text", text, text_elements: [] }];
|
|
818
|
+
}
|
|
819
|
+
function normalizeAppServerItem(raw) {
|
|
820
|
+
const id = typeof raw.id === "string" ? raw.id : "unknown";
|
|
821
|
+
switch (raw.type) {
|
|
822
|
+
case "agentMessage":
|
|
823
|
+
return { id, type: "agent_message", text: typeof raw.text === "string" ? raw.text : "" };
|
|
824
|
+
case "reasoning":
|
|
825
|
+
return {
|
|
826
|
+
id,
|
|
827
|
+
type: "reasoning",
|
|
828
|
+
summary: Array.isArray(raw.summary) ? raw.summary.filter((v) => typeof v === "string") : [],
|
|
829
|
+
content: Array.isArray(raw.content) ? raw.content.filter((v) => typeof v === "string") : []
|
|
830
|
+
};
|
|
831
|
+
case "plan":
|
|
832
|
+
return { id, type: "todo_list", items: typeof raw.text === "string" ? [raw.text] : [] };
|
|
833
|
+
case "commandExecution":
|
|
834
|
+
return {
|
|
835
|
+
id,
|
|
836
|
+
type: "command_execution",
|
|
837
|
+
command: typeof raw.command === "string" ? raw.command : "",
|
|
838
|
+
aggregated_output: typeof raw.aggregatedOutput === "string" ? raw.aggregatedOutput : null,
|
|
839
|
+
exit_code: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
840
|
+
status: typeof raw.status === "string" ? raw.status : undefined
|
|
841
|
+
};
|
|
842
|
+
case "fileChange":
|
|
843
|
+
return {
|
|
844
|
+
id,
|
|
845
|
+
type: "file_change",
|
|
846
|
+
changes: Array.isArray(raw.changes) ? raw.changes.flatMap((change) => {
|
|
847
|
+
if (!change || typeof change !== "object")
|
|
848
|
+
return [];
|
|
849
|
+
const value = change;
|
|
850
|
+
const kindValue = value.kind;
|
|
851
|
+
const kind = typeof kindValue === "string" ? kindValue : kindValue && typeof kindValue === "object" ? kindValue.type : undefined;
|
|
852
|
+
return [
|
|
853
|
+
{
|
|
854
|
+
kind: typeof kind === "string" ? kind : "update",
|
|
855
|
+
path: typeof value.path === "string" ? value.path : ""
|
|
856
|
+
}
|
|
857
|
+
];
|
|
858
|
+
}) : [],
|
|
859
|
+
status: typeof raw.status === "string" ? raw.status : undefined
|
|
860
|
+
};
|
|
861
|
+
case "mcpToolCall":
|
|
862
|
+
return {
|
|
863
|
+
id,
|
|
864
|
+
type: "mcp_tool_call",
|
|
865
|
+
server: typeof raw.server === "string" ? raw.server : "",
|
|
866
|
+
tool: typeof raw.tool === "string" ? raw.tool : "",
|
|
867
|
+
arguments: raw.arguments,
|
|
868
|
+
status: typeof raw.status === "string" ? raw.status : undefined,
|
|
869
|
+
result: raw.result,
|
|
870
|
+
error: raw.error
|
|
871
|
+
};
|
|
872
|
+
case "webSearch":
|
|
873
|
+
return { id, type: "web_search", query: typeof raw.query === "string" ? raw.query : "" };
|
|
874
|
+
default:
|
|
875
|
+
return {
|
|
876
|
+
id,
|
|
877
|
+
type: "unknown",
|
|
878
|
+
originalType: typeof raw.type === "string" ? raw.type : undefined,
|
|
879
|
+
value: raw
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function normalizeUsage(raw) {
|
|
884
|
+
if (!raw || typeof raw !== "object")
|
|
885
|
+
return;
|
|
886
|
+
const value = raw;
|
|
887
|
+
return {
|
|
888
|
+
input_tokens: typeof value.inputTokens === "number" ? value.inputTokens : 0,
|
|
889
|
+
cached_input_tokens: typeof value.cachedInputTokens === "number" ? value.cachedInputTokens : 0,
|
|
890
|
+
cache_write_input_tokens: typeof value.cacheWriteInputTokens === "number" ? value.cacheWriteInputTokens : 0,
|
|
891
|
+
output_tokens: typeof value.outputTokens === "number" ? value.outputTokens : 0,
|
|
892
|
+
reasoning_output_tokens: typeof value.reasoningOutputTokens === "number" ? value.reasoningOutputTokens : 0
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function subtractUsage(total, previous) {
|
|
896
|
+
return {
|
|
897
|
+
input_tokens: Math.max(0, total.input_tokens - (previous?.input_tokens ?? 0)),
|
|
898
|
+
cached_input_tokens: Math.max(0, total.cached_input_tokens - (previous?.cached_input_tokens ?? 0)),
|
|
899
|
+
cache_write_input_tokens: Math.max(0, total.cache_write_input_tokens - (previous?.cache_write_input_tokens ?? 0)),
|
|
900
|
+
output_tokens: Math.max(0, total.output_tokens - (previous?.output_tokens ?? 0)),
|
|
901
|
+
reasoning_output_tokens: Math.max(0, total.reasoning_output_tokens - (previous?.reasoning_output_tokens ?? 0))
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
class AppServerCodexThread {
|
|
906
|
+
client;
|
|
907
|
+
id;
|
|
908
|
+
get version() {
|
|
909
|
+
return this.client.version;
|
|
910
|
+
}
|
|
911
|
+
activeTurnId = null;
|
|
912
|
+
activePush = null;
|
|
913
|
+
activeFinish = null;
|
|
914
|
+
activeFail = null;
|
|
915
|
+
latestTotalUsage;
|
|
916
|
+
completedTotalUsage;
|
|
917
|
+
emittedThreadStart = false;
|
|
918
|
+
unsubscribe;
|
|
919
|
+
unsubscribeClose;
|
|
920
|
+
turnWaiters = [];
|
|
921
|
+
constructor(client, threadId) {
|
|
922
|
+
this.client = client;
|
|
923
|
+
this.id = threadId;
|
|
924
|
+
this.unsubscribe = client.onNotification((notification) => this.handleNotification(notification));
|
|
925
|
+
this.unsubscribeClose = client.onClose((error) => this.activeFail?.(error));
|
|
926
|
+
}
|
|
927
|
+
async runStreamed(input) {
|
|
928
|
+
if (this.activePush)
|
|
929
|
+
throw new Error("Codex turn already active");
|
|
930
|
+
const pending = [];
|
|
931
|
+
let wake = null;
|
|
932
|
+
let finished = false;
|
|
933
|
+
let failure = null;
|
|
934
|
+
const push = (event) => {
|
|
935
|
+
pending.push(event);
|
|
936
|
+
wake?.();
|
|
937
|
+
wake = null;
|
|
938
|
+
};
|
|
939
|
+
const finish = () => {
|
|
940
|
+
finished = true;
|
|
941
|
+
wake?.();
|
|
942
|
+
wake = null;
|
|
943
|
+
};
|
|
944
|
+
this.activePush = push;
|
|
945
|
+
this.activeFinish = finish;
|
|
946
|
+
this.activeFail = (error) => {
|
|
947
|
+
failure = error;
|
|
948
|
+
this.activePush = null;
|
|
949
|
+
this.activeFinish = null;
|
|
950
|
+
finish();
|
|
951
|
+
};
|
|
952
|
+
if (!this.emittedThreadStart) {
|
|
953
|
+
this.emittedThreadStart = true;
|
|
954
|
+
push({ type: "thread.started", thread_id: this.id });
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const response = await this.client.request("turn/start", {
|
|
958
|
+
threadId: this.id,
|
|
959
|
+
input: textInput(input)
|
|
960
|
+
});
|
|
961
|
+
if (!finished)
|
|
962
|
+
this.setActiveTurn(response.turn.id);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
this.activePush = null;
|
|
965
|
+
this.activeFinish = null;
|
|
966
|
+
this.activeFail = null;
|
|
967
|
+
finish();
|
|
968
|
+
throw error;
|
|
969
|
+
}
|
|
970
|
+
const events = async function* () {
|
|
971
|
+
while (!finished || pending.length > 0) {
|
|
972
|
+
if (pending.length === 0) {
|
|
973
|
+
await new Promise((resolve2) => {
|
|
974
|
+
wake = resolve2;
|
|
975
|
+
});
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
yield pending.shift();
|
|
979
|
+
}
|
|
980
|
+
if (failure)
|
|
981
|
+
throw failure;
|
|
982
|
+
};
|
|
983
|
+
return { events: events() };
|
|
984
|
+
}
|
|
985
|
+
async steer(input) {
|
|
986
|
+
const turnId = this.activeTurnId ?? await new Promise((resolve2, reject) => this.turnWaiters.push({ resolve: resolve2, reject }));
|
|
987
|
+
await this.client.request("turn/steer", {
|
|
988
|
+
threadId: this.id,
|
|
989
|
+
expectedTurnId: turnId,
|
|
990
|
+
input: textInput(input)
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
async interrupt() {
|
|
994
|
+
if (!this.activeTurnId)
|
|
995
|
+
return;
|
|
996
|
+
await this.client.request("turn/interrupt", {
|
|
997
|
+
threadId: this.id,
|
|
998
|
+
turnId: this.activeTurnId
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
async close() {
|
|
1002
|
+
for (const waiter of this.turnWaiters.splice(0)) {
|
|
1003
|
+
waiter.reject(new Error("Codex session has completed"));
|
|
1004
|
+
}
|
|
1005
|
+
this.unsubscribe();
|
|
1006
|
+
this.unsubscribeClose();
|
|
1007
|
+
await this.client.close();
|
|
1008
|
+
}
|
|
1009
|
+
handleNotification(notification) {
|
|
1010
|
+
const params = notification.params && typeof notification.params === "object" ? notification.params : {};
|
|
1011
|
+
if (params.threadId !== undefined && params.threadId !== this.id)
|
|
1012
|
+
return;
|
|
1013
|
+
const push = this.activePush;
|
|
1014
|
+
if (!push)
|
|
1015
|
+
return;
|
|
1016
|
+
switch (notification.method) {
|
|
1017
|
+
case "turn/started": {
|
|
1018
|
+
const turn = params.turn;
|
|
1019
|
+
if (typeof turn?.id === "string")
|
|
1020
|
+
this.setActiveTurn(turn.id);
|
|
1021
|
+
push({ type: "turn.started", turn_id: this.activeTurnId ?? undefined });
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
case "item/started":
|
|
1025
|
+
case "item/completed": {
|
|
1026
|
+
if (!params.item || typeof params.item !== "object")
|
|
1027
|
+
break;
|
|
1028
|
+
push({
|
|
1029
|
+
type: notification.method === "item/started" ? "item.started" : "item.completed",
|
|
1030
|
+
item: normalizeAppServerItem(params.item)
|
|
1031
|
+
});
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
case "item/agentMessage/delta":
|
|
1035
|
+
if (typeof params.itemId === "string" && typeof params.delta === "string") {
|
|
1036
|
+
push({ type: "message.delta", item_id: params.itemId, delta: params.delta });
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
case "thread/tokenUsage/updated": {
|
|
1040
|
+
const tokenUsage = params.tokenUsage;
|
|
1041
|
+
this.latestTotalUsage = normalizeUsage(tokenUsage?.total);
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
case "error": {
|
|
1045
|
+
const error = params.error;
|
|
1046
|
+
if (typeof error?.message === "string") {
|
|
1047
|
+
push({
|
|
1048
|
+
type: "error",
|
|
1049
|
+
message: error.message,
|
|
1050
|
+
willRetry: params.willRetry === true,
|
|
1051
|
+
codexErrorInfo: typeof error.codexErrorInfo === "string" ? error.codexErrorInfo : null
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
case "turn/completed": {
|
|
1057
|
+
const turn = params.turn;
|
|
1058
|
+
if (typeof turn?.id === "string" && this.activeTurnId && turn.id !== this.activeTurnId)
|
|
1059
|
+
break;
|
|
1060
|
+
const usage = this.latestTotalUsage ? subtractUsage(this.latestTotalUsage, this.completedTotalUsage) : undefined;
|
|
1061
|
+
if (this.latestTotalUsage)
|
|
1062
|
+
this.completedTotalUsage = this.latestTotalUsage;
|
|
1063
|
+
if (turn?.status === "failed") {
|
|
1064
|
+
push({
|
|
1065
|
+
type: "turn.failed",
|
|
1066
|
+
error: {
|
|
1067
|
+
message: typeof turn.error?.message === "string" ? turn.error.message : "Codex turn failed",
|
|
1068
|
+
codexErrorInfo: typeof turn.error?.codexErrorInfo === "string" ? turn.error.codexErrorInfo : null
|
|
1069
|
+
},
|
|
1070
|
+
usage
|
|
1071
|
+
});
|
|
1072
|
+
} else if (turn?.status === "interrupted") {
|
|
1073
|
+
push({ type: "turn.interrupted", usage });
|
|
1074
|
+
} else {
|
|
1075
|
+
push({ type: "turn.completed", usage });
|
|
1076
|
+
}
|
|
1077
|
+
this.activeTurnId = null;
|
|
1078
|
+
for (const waiter of this.turnWaiters.splice(0)) {
|
|
1079
|
+
waiter.reject(new Error("Codex turn has completed"));
|
|
1080
|
+
}
|
|
1081
|
+
this.activePush = null;
|
|
1082
|
+
this.activeFail = null;
|
|
1083
|
+
this.activeFinish?.();
|
|
1084
|
+
this.activeFinish = null;
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
setActiveTurn(turnId) {
|
|
1090
|
+
this.activeTurnId = turnId;
|
|
1091
|
+
for (const waiter of this.turnWaiters.splice(0))
|
|
1092
|
+
waiter.resolve(turnId);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
class CodexSession {
|
|
1097
|
+
thread;
|
|
1098
|
+
config;
|
|
1099
|
+
agentsMdHandle;
|
|
1100
|
+
resolvedModel;
|
|
1101
|
+
contextWindow;
|
|
1102
|
+
skillsDir;
|
|
1103
|
+
summarizeDeps;
|
|
1104
|
+
listeners = [];
|
|
1105
|
+
eventQueue = [];
|
|
1106
|
+
logFileHandle;
|
|
1107
|
+
startedAt = Date.now();
|
|
1108
|
+
completionPromise;
|
|
1109
|
+
resolveCompletion;
|
|
1110
|
+
abortController = null;
|
|
1111
|
+
abortReason = "cancelled";
|
|
1112
|
+
queuedPrompts = [];
|
|
1113
|
+
transcript = [];
|
|
1114
|
+
abortRef = { current: null };
|
|
1115
|
+
_sessionId;
|
|
1116
|
+
numTurns = 0;
|
|
1117
|
+
accumulatedUsage = null;
|
|
1118
|
+
aborted = false;
|
|
1119
|
+
settled = false;
|
|
1120
|
+
acceptsSteering = true;
|
|
1121
|
+
errorTracker = new SessionErrorTracker;
|
|
1122
|
+
appliedReasoningEffort;
|
|
1123
|
+
pendingResult = null;
|
|
1124
|
+
constructor(thread, config, agentsMdHandle, resolvedModel, initialEvents = [], skillsDir, summarizeDeps = {}) {
|
|
1125
|
+
this.thread = thread;
|
|
1126
|
+
this.config = config;
|
|
1127
|
+
this.agentsMdHandle = agentsMdHandle;
|
|
1128
|
+
this.resolvedModel = resolvedModel;
|
|
1129
|
+
this.contextWindow = getCodexContextWindow(resolvedModel);
|
|
1130
|
+
this.skillsDir = skillsDir ?? process.env.CODEX_SKILLS_DIR ?? join2(codexUserHome(), ".codex", "skills");
|
|
1131
|
+
this.summarizeDeps = summarizeDeps;
|
|
1132
|
+
this.logFileHandle = Bun.file(config.logFile).writer();
|
|
1133
|
+
const reasoningApplication = applyReasoningEffort("codex", resolvedModel, config.reasoningEffort);
|
|
1134
|
+
this.appliedReasoningEffort = reasoningApplication.kind === "codex-config" ? config.reasoningEffort ?? null : null;
|
|
1135
|
+
this.completionPromise = new Promise((resolve2) => {
|
|
1136
|
+
this.resolveCompletion = resolve2;
|
|
1137
|
+
});
|
|
1138
|
+
if (config.taskId && config.apiUrl && config.apiKey) {
|
|
1139
|
+
this.listeners.push(createCodexSwarmEventHandler({
|
|
1140
|
+
apiUrl: config.apiUrl,
|
|
1141
|
+
apiKey: config.apiKey,
|
|
1142
|
+
agentId: config.agentId,
|
|
1143
|
+
taskId: config.taskId,
|
|
1144
|
+
abortRef: this.abortRef
|
|
1145
|
+
}));
|
|
1146
|
+
}
|
|
1147
|
+
for (const event of initialEvents) {
|
|
1148
|
+
this.emit(event);
|
|
1149
|
+
}
|
|
1150
|
+
this.runSession();
|
|
1151
|
+
}
|
|
1152
|
+
get sessionId() {
|
|
1153
|
+
return this._sessionId ?? this.thread.id ?? undefined;
|
|
1154
|
+
}
|
|
1155
|
+
onEvent(listener) {
|
|
1156
|
+
this.listeners.push(listener);
|
|
1157
|
+
for (const event of this.eventQueue) {
|
|
1158
|
+
listener(event);
|
|
1159
|
+
}
|
|
1160
|
+
this.eventQueue.length = 0;
|
|
1161
|
+
}
|
|
1162
|
+
async waitForCompletion() {
|
|
1163
|
+
return this.completionPromise;
|
|
1164
|
+
}
|
|
1165
|
+
emitTransportStderr(line) {
|
|
1166
|
+
this.emit({ type: "raw_stderr", content: `${line}
|
|
1167
|
+
` });
|
|
1168
|
+
}
|
|
1169
|
+
async abort(reason) {
|
|
1170
|
+
if (this.aborted || this.settled)
|
|
1171
|
+
return;
|
|
1172
|
+
this.aborted = true;
|
|
1173
|
+
this.abortReason = reason ?? "cancelled";
|
|
1174
|
+
if (this.abortController) {
|
|
1175
|
+
this.abortController.abort(this.abortReason);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
await this.thread.interrupt();
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
this.emit({
|
|
1182
|
+
type: "raw_stderr",
|
|
1183
|
+
content: `[codex] Failed to interrupt turn: ${String(error)}
|
|
1184
|
+
`
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async deliverSteering({ mode, text }) {
|
|
1189
|
+
if (!this.acceptsSteering || this.settled || this.aborted) {
|
|
1190
|
+
return { delivered: false, reason: "Codex session has completed" };
|
|
1191
|
+
}
|
|
1192
|
+
if (mode === "queue") {
|
|
1193
|
+
return new Promise((resolve2) => this.queuedPrompts.push({ text, resolve: resolve2 }));
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
await this.thread.steer(text);
|
|
1197
|
+
return { delivered: true, mode: "steer" };
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
return { delivered: false, reason: scrubSecrets(String(error)) };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
emit(event) {
|
|
1203
|
+
const scrubbed = JSON.parse(scrubSecrets(JSON.stringify(event)));
|
|
1204
|
+
try {
|
|
1205
|
+
this.logFileHandle.write(`${JSON.stringify({ ...scrubbed, timestamp: new Date().toISOString() })}
|
|
1206
|
+
`);
|
|
1207
|
+
} catch {}
|
|
1208
|
+
if (this.listeners.length > 0) {
|
|
1209
|
+
for (const listener of this.listeners) {
|
|
1210
|
+
try {
|
|
1211
|
+
listener(scrubbed);
|
|
1212
|
+
} catch {}
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
this.eventQueue.push(scrubbed);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
settle(result) {
|
|
1219
|
+
if (this.settled)
|
|
1220
|
+
return;
|
|
1221
|
+
this.settled = true;
|
|
1222
|
+
for (const queued of this.queuedPrompts.splice(0)) {
|
|
1223
|
+
queued.resolve({
|
|
1224
|
+
delivered: false,
|
|
1225
|
+
reason: "Codex session ended before the queued turn started"
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
this.pendingResult = { ...result, appliedReasoningEffort: this.appliedReasoningEffort };
|
|
1229
|
+
}
|
|
1230
|
+
buildCostData(usage, isError) {
|
|
1231
|
+
const inputTokens = usage?.input_tokens ?? 0;
|
|
1232
|
+
const cachedInputTokens = usage?.cached_input_tokens ?? 0;
|
|
1233
|
+
const outputTokens = usage?.output_tokens ?? 0;
|
|
1234
|
+
const reasoningOutputTokens = usage?.reasoning_output_tokens ?? 0;
|
|
1235
|
+
return {
|
|
1236
|
+
sessionId: "",
|
|
1237
|
+
taskId: this.config.taskId,
|
|
1238
|
+
agentId: this.config.agentId,
|
|
1239
|
+
totalCostUsd: computeCodexCostUsd(this.resolvedModel, inputTokens, cachedInputTokens, outputTokens),
|
|
1240
|
+
inputTokens,
|
|
1241
|
+
outputTokens,
|
|
1242
|
+
reasoningOutputTokens,
|
|
1243
|
+
cacheReadTokens: cachedInputTokens,
|
|
1244
|
+
cacheWriteTokens: usage?.cache_write_input_tokens,
|
|
1245
|
+
durationMs: Date.now() - this.startedAt,
|
|
1246
|
+
numTurns: this.numTurns,
|
|
1247
|
+
model: this.resolvedModel,
|
|
1248
|
+
isError,
|
|
1249
|
+
provider: "codex"
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
toolNameForItem(item) {
|
|
1253
|
+
switch (item.type) {
|
|
1254
|
+
case "command_execution":
|
|
1255
|
+
return "bash";
|
|
1256
|
+
case "file_change": {
|
|
1257
|
+
const first = item.changes[0];
|
|
1258
|
+
if (!first)
|
|
1259
|
+
return "Edit";
|
|
1260
|
+
return first.kind === "add" ? "Write" : first.kind === "delete" ? "Delete" : "Edit";
|
|
1261
|
+
}
|
|
1262
|
+
case "mcp_tool_call":
|
|
1263
|
+
return item.tool;
|
|
1264
|
+
case "web_search":
|
|
1265
|
+
return "WebSearch";
|
|
1266
|
+
default:
|
|
1267
|
+
return item.type;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
toolArgsForItem(item) {
|
|
1271
|
+
switch (item.type) {
|
|
1272
|
+
case "command_execution":
|
|
1273
|
+
return { command: item.command };
|
|
1274
|
+
case "file_change":
|
|
1275
|
+
return { changes: item.changes };
|
|
1276
|
+
case "mcp_tool_call": {
|
|
1277
|
+
const mcpItem = item;
|
|
1278
|
+
return { server: mcpItem.server, tool: mcpItem.tool, arguments: mcpItem.arguments };
|
|
1279
|
+
}
|
|
1280
|
+
case "web_search":
|
|
1281
|
+
return { query: item.query };
|
|
1282
|
+
default:
|
|
1283
|
+
return {};
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
isToolItem(item) {
|
|
1287
|
+
return item.type === "command_execution" || item.type === "file_change" || item.type === "mcp_tool_call" || item.type === "web_search";
|
|
1288
|
+
}
|
|
1289
|
+
shortenItemResult(item) {
|
|
1290
|
+
switch (item.type) {
|
|
1291
|
+
case "command_execution": {
|
|
1292
|
+
const cmd = item;
|
|
1293
|
+
const stdout = (cmd.aggregated_output ?? "").slice(0, 500);
|
|
1294
|
+
return `exit=${cmd.exit_code ?? "?"} status=${cmd.status ?? "?"} stdout=${stdout}`;
|
|
1295
|
+
}
|
|
1296
|
+
case "file_change": {
|
|
1297
|
+
const fc = item;
|
|
1298
|
+
const summarised = (fc.changes ?? []).slice(0, 5).map((c) => `${c.kind}:${c.path}`).join(",");
|
|
1299
|
+
return `changes=[${summarised}]`;
|
|
1300
|
+
}
|
|
1301
|
+
case "mcp_tool_call": {
|
|
1302
|
+
const mcp = item;
|
|
1303
|
+
return `server=${mcp.server} tool=${mcp.tool} status=${mcp.status ?? "?"}`;
|
|
1304
|
+
}
|
|
1305
|
+
case "web_search": {
|
|
1306
|
+
const ws = item;
|
|
1307
|
+
return `query=${(ws.query ?? "").slice(0, 200)}`;
|
|
1308
|
+
}
|
|
1309
|
+
default:
|
|
1310
|
+
return JSON.stringify(item).slice(0, 500);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
handleEvent(event) {
|
|
1314
|
+
this.emit({ type: "raw_log", content: JSON.stringify(event) });
|
|
1315
|
+
switch (event.type) {
|
|
1316
|
+
case "thread.started": {
|
|
1317
|
+
this._sessionId = event.thread_id;
|
|
1318
|
+
const codexVersion = this.thread.version ?? readPkgVersion("@openai/codex");
|
|
1319
|
+
this.emit({
|
|
1320
|
+
type: "session_init",
|
|
1321
|
+
sessionId: event.thread_id,
|
|
1322
|
+
provider: "codex",
|
|
1323
|
+
...codexVersion ? { harnessVariantMeta: { version: codexVersion } } : {}
|
|
1324
|
+
});
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
case "turn.started": {
|
|
1328
|
+
this.numTurns += 1;
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
case "item.started": {
|
|
1332
|
+
if (this.isToolItem(event.item)) {
|
|
1333
|
+
this.emit({
|
|
1334
|
+
type: "tool_start",
|
|
1335
|
+
toolCallId: event.item.id,
|
|
1336
|
+
toolName: this.toolNameForItem(event.item),
|
|
1337
|
+
args: this.toolArgsForItem(event.item)
|
|
1338
|
+
});
|
|
1339
|
+
this.transcript.push(`Tool[${this.toolNameForItem(event.item)}] started: ${JSON.stringify(this.toolArgsForItem(event.item)).slice(0, 500)}`);
|
|
1340
|
+
}
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
case "item.updated": {
|
|
1344
|
+
const updatedItem = event.item;
|
|
1345
|
+
if (updatedItem.type === "agent_message") {
|
|
1346
|
+
const msg = updatedItem;
|
|
1347
|
+
if (msg.text) {
|
|
1348
|
+
this.emit({
|
|
1349
|
+
type: "custom",
|
|
1350
|
+
name: "codex.message_delta",
|
|
1351
|
+
data: { itemId: updatedItem.id, text: msg.text }
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
case "message.delta": {
|
|
1358
|
+
this.emit({
|
|
1359
|
+
type: "custom",
|
|
1360
|
+
name: "codex.message_delta",
|
|
1361
|
+
data: { itemId: event.item_id, text: event.delta }
|
|
1362
|
+
});
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
case "item.completed": {
|
|
1366
|
+
const { item } = event;
|
|
1367
|
+
if (this.isToolItem(item)) {
|
|
1368
|
+
this.emit({
|
|
1369
|
+
type: "tool_end",
|
|
1370
|
+
toolCallId: item.id,
|
|
1371
|
+
toolName: this.toolNameForItem(item),
|
|
1372
|
+
result: item
|
|
1373
|
+
});
|
|
1374
|
+
this.transcript.push(`Tool[${this.toolNameForItem(item)}] completed: ${this.shortenItemResult(item)}`);
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
switch (item.type) {
|
|
1378
|
+
case "agent_message": {
|
|
1379
|
+
const msg = item;
|
|
1380
|
+
if (msg.text) {
|
|
1381
|
+
this.emit({ type: "message", role: "assistant", content: msg.text });
|
|
1382
|
+
this.transcript.push(`Assistant: ${msg.text}`);
|
|
1383
|
+
}
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
case "reasoning": {
|
|
1387
|
+
const r = item;
|
|
1388
|
+
const summary = Array.isArray(r.summary) ? r.summary.join(`
|
|
1389
|
+
`) : r.summary;
|
|
1390
|
+
const content = Array.isArray(r.content) ? r.content.join(`
|
|
1391
|
+
`) : undefined;
|
|
1392
|
+
const text = r.text || summary || content || "";
|
|
1393
|
+
if (text) {
|
|
1394
|
+
this.emit({
|
|
1395
|
+
type: "custom",
|
|
1396
|
+
name: "codex.reasoning",
|
|
1397
|
+
data: { itemId: r.id, text }
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
case "todo_list": {
|
|
1403
|
+
const todo = item;
|
|
1404
|
+
this.emit({
|
|
1405
|
+
type: "custom",
|
|
1406
|
+
name: "codex.todo_list",
|
|
1407
|
+
data: { itemId: todo.id, items: todo.items ?? [] }
|
|
1408
|
+
});
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
case "error": {
|
|
1412
|
+
const errItem = item;
|
|
1413
|
+
this.emit({
|
|
1414
|
+
type: "error",
|
|
1415
|
+
message: this.formatTerminalError(errItem.message).message
|
|
1416
|
+
});
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
case "turn.completed": {
|
|
1423
|
+
this.accountUsage(event.usage);
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
case "turn.failed": {
|
|
1427
|
+
this.accountUsage(event.usage);
|
|
1428
|
+
const { message } = this.formatTerminalError(event.error.message, event.error.codexErrorInfo);
|
|
1429
|
+
this.emit({ type: "error", message });
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
case "turn.interrupted":
|
|
1433
|
+
this.accountUsage(event.usage);
|
|
1434
|
+
break;
|
|
1435
|
+
case "error": {
|
|
1436
|
+
const { message } = this.formatTerminalError(event.message, event.codexErrorInfo);
|
|
1437
|
+
if (event.willRetry === true) {
|
|
1438
|
+
this.emit({ type: "raw_stderr", content: `[codex] ${message} (retrying)
|
|
1439
|
+
` });
|
|
1440
|
+
} else {
|
|
1441
|
+
this.emit({ type: "error", message });
|
|
1442
|
+
}
|
|
1443
|
+
break;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
accountUsage(usage) {
|
|
1448
|
+
if (!usage)
|
|
1449
|
+
return;
|
|
1450
|
+
this.accumulatedUsage = this.accumulatedUsage ? {
|
|
1451
|
+
input_tokens: this.accumulatedUsage.input_tokens + usage.input_tokens,
|
|
1452
|
+
cached_input_tokens: this.accumulatedUsage.cached_input_tokens + usage.cached_input_tokens,
|
|
1453
|
+
cache_write_input_tokens: this.accumulatedUsage.cache_write_input_tokens + usage.cache_write_input_tokens,
|
|
1454
|
+
output_tokens: this.accumulatedUsage.output_tokens + usage.output_tokens,
|
|
1455
|
+
reasoning_output_tokens: this.accumulatedUsage.reasoning_output_tokens + usage.reasoning_output_tokens
|
|
1456
|
+
} : usage;
|
|
1457
|
+
const contextUsed = computeContextUsedUnified({
|
|
1458
|
+
inputTokens: usage.input_tokens,
|
|
1459
|
+
cacheReadTokens: 0,
|
|
1460
|
+
cacheCreateTokens: 0,
|
|
1461
|
+
outputTokens: usage.output_tokens
|
|
1462
|
+
});
|
|
1463
|
+
this.emit({
|
|
1464
|
+
type: "context_usage",
|
|
1465
|
+
contextUsedTokens: contextUsed,
|
|
1466
|
+
contextTotalTokens: this.contextWindow,
|
|
1467
|
+
contextPercent: clampContextPercent(contextUsed, this.contextWindow) ?? 0,
|
|
1468
|
+
outputTokens: usage.output_tokens,
|
|
1469
|
+
contextFormula: CONTEXT_FORMULA
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
formatTerminalError(raw, codexErrorInfo) {
|
|
1473
|
+
const normalized = `${codexErrorInfo ?? ""} ${raw}`.toLowerCase();
|
|
1474
|
+
const overflowPatterns = [
|
|
1475
|
+
"context length exceeded",
|
|
1476
|
+
"maximum context length",
|
|
1477
|
+
"too many tokens",
|
|
1478
|
+
"input too long",
|
|
1479
|
+
"request too large",
|
|
1480
|
+
"context_length_exceeded",
|
|
1481
|
+
"contextwindowexceeded"
|
|
1482
|
+
];
|
|
1483
|
+
if (overflowPatterns.some((p) => normalized.includes(p))) {
|
|
1484
|
+
return {
|
|
1485
|
+
message: `[context-overflow] Codex turn exceeded the model's context window for ${this.resolvedModel} (${this.contextWindow.toLocaleString()} tokens). Codex does not auto-compact conversation history like Claude does — start a fresh task or split the work into smaller turns. Original error: ${raw}`,
|
|
1486
|
+
category: "context_overflow"
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
const usageLimitPatterns = ["usage limit", "upgrade to pro", "usagelimitexceeded"];
|
|
1490
|
+
if (usageLimitPatterns.some((p) => normalized.includes(p))) {
|
|
1491
|
+
return {
|
|
1492
|
+
message: `[usage-limit] Codex account quota exhausted — upgrade plan or wait for monthly reset. Original error: ${raw}`,
|
|
1493
|
+
category: "usage_limit"
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
const rateLimitPatterns = [
|
|
1497
|
+
"rate limit",
|
|
1498
|
+
"rate_limit",
|
|
1499
|
+
"ratelimit",
|
|
1500
|
+
"too many requests",
|
|
1501
|
+
"http 429",
|
|
1502
|
+
" 429 "
|
|
1503
|
+
];
|
|
1504
|
+
if (rateLimitPatterns.some((p) => normalized.includes(p))) {
|
|
1505
|
+
return {
|
|
1506
|
+
message: `[rate-limit] Codex API rate limit hit. Original error: ${raw}`,
|
|
1507
|
+
category: "rate_limit"
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
const authPatterns = [
|
|
1511
|
+
"unauthorized",
|
|
1512
|
+
"http 401",
|
|
1513
|
+
" 401 ",
|
|
1514
|
+
"invalid api key",
|
|
1515
|
+
"invalid_api_key",
|
|
1516
|
+
"missing api key",
|
|
1517
|
+
"no api key",
|
|
1518
|
+
"authentication failed",
|
|
1519
|
+
"failed to refresh token",
|
|
1520
|
+
"invalid 'refresh_token'"
|
|
1521
|
+
];
|
|
1522
|
+
if (authPatterns.some((p) => normalized.includes(p))) {
|
|
1523
|
+
return {
|
|
1524
|
+
message: `[auth-error] Codex authentication failed — check OPENAI_API_KEY or ChatGPT login. Original error: ${raw}`,
|
|
1525
|
+
category: "authentication_failed"
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
return { message: raw };
|
|
1529
|
+
}
|
|
1530
|
+
async runSession() {
|
|
1531
|
+
this.abortController = new AbortController;
|
|
1532
|
+
this.abortController.signal.addEventListener("abort", () => {
|
|
1533
|
+
this.aborted = true;
|
|
1534
|
+
if (typeof this.abortController?.signal.reason === "string") {
|
|
1535
|
+
this.abortReason = this.abortController.signal.reason;
|
|
1536
|
+
}
|
|
1537
|
+
this.thread.interrupt().catch((error) => {
|
|
1538
|
+
this.emit({
|
|
1539
|
+
type: "raw_stderr",
|
|
1540
|
+
content: `[codex] Failed to interrupt turn: ${String(error)}
|
|
1541
|
+
`
|
|
1542
|
+
});
|
|
1543
|
+
});
|
|
1544
|
+
}, { once: true });
|
|
1545
|
+
this.abortRef.current = this.abortController;
|
|
1546
|
+
let terminalError;
|
|
1547
|
+
let sawTurnCompleted = false;
|
|
1548
|
+
let sawTurnInterrupted = false;
|
|
1549
|
+
try {
|
|
1550
|
+
const resolvedPrompt = await resolveCodexPrompt(this.config.prompt, this.skillsDir, (event) => this.emit(event));
|
|
1551
|
+
this.transcript = [`User: ${resolvedPrompt}`];
|
|
1552
|
+
try {
|
|
1553
|
+
let nextPrompt = resolvedPrompt;
|
|
1554
|
+
let queuedPrompt;
|
|
1555
|
+
while (nextPrompt !== undefined && !this.aborted && !terminalError && !sawTurnInterrupted) {
|
|
1556
|
+
if (nextPrompt !== resolvedPrompt)
|
|
1557
|
+
this.transcript.push(`User: ${nextPrompt}`);
|
|
1558
|
+
const streamed = await this.thread.runStreamed(nextPrompt);
|
|
1559
|
+
if (queuedPrompt) {
|
|
1560
|
+
this.queuedPrompts.shift();
|
|
1561
|
+
queuedPrompt.resolve({ delivered: true, mode: "queue" });
|
|
1562
|
+
}
|
|
1563
|
+
for await (const event of streamed.events) {
|
|
1564
|
+
if (event.type === "turn.failed" || event.type === "turn.interrupted" || event.type === "error" && event.willRetry !== true) {
|
|
1565
|
+
this.acceptsSteering = false;
|
|
1566
|
+
}
|
|
1567
|
+
this.handleEvent(event);
|
|
1568
|
+
if (event.type === "turn.completed") {
|
|
1569
|
+
sawTurnCompleted = true;
|
|
1570
|
+
}
|
|
1571
|
+
if (event.type === "turn.interrupted") {
|
|
1572
|
+
sawTurnInterrupted = true;
|
|
1573
|
+
}
|
|
1574
|
+
if (event.type === "turn.failed" && !terminalError) {
|
|
1575
|
+
terminalError = this.formatTerminalError(event.error.message, event.error.codexErrorInfo);
|
|
1576
|
+
this.errorTracker.processCodexUsageLimitMessage(event.error.message);
|
|
1577
|
+
}
|
|
1578
|
+
if (event.type === "error" && event.willRetry !== true && !terminalError) {
|
|
1579
|
+
terminalError = this.formatTerminalError(event.message, event.codexErrorInfo);
|
|
1580
|
+
this.errorTracker.processCodexUsageLimitMessage(event.message);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
queuedPrompt = this.queuedPrompts[0];
|
|
1584
|
+
nextPrompt = queuedPrompt?.text;
|
|
1585
|
+
}
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
this.acceptsSteering = false;
|
|
1588
|
+
if (this.aborted || err instanceof Error && err.name === "AbortError") {
|
|
1589
|
+
const cost2 = this.buildCostData(this.accumulatedUsage, true);
|
|
1590
|
+
this.emit({ type: "result", cost: cost2, isError: true, errorCategory: "cancelled" });
|
|
1591
|
+
this.settle({
|
|
1592
|
+
exitCode: 130,
|
|
1593
|
+
sessionId: this._sessionId,
|
|
1594
|
+
cost: cost2,
|
|
1595
|
+
isError: true,
|
|
1596
|
+
failureReason: this.abortReason
|
|
1597
|
+
});
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (terminalError) {
|
|
1601
|
+
const cost2 = this.buildCostData(this.accumulatedUsage, true);
|
|
1602
|
+
this.emit({
|
|
1603
|
+
type: "result",
|
|
1604
|
+
cost: cost2,
|
|
1605
|
+
isError: true,
|
|
1606
|
+
errorCategory: terminalError.category ?? "turn_failed"
|
|
1607
|
+
});
|
|
1608
|
+
this.settle({
|
|
1609
|
+
exitCode: 1,
|
|
1610
|
+
sessionId: this._sessionId,
|
|
1611
|
+
cost: cost2,
|
|
1612
|
+
isError: true,
|
|
1613
|
+
failureReason: terminalError.message,
|
|
1614
|
+
rateLimitResetAt: this.errorTracker.getRateLimitResetAt(),
|
|
1615
|
+
rateLimitWindows: this.errorTracker.getRateLimitWindows()
|
|
1616
|
+
});
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
throw err;
|
|
1620
|
+
}
|
|
1621
|
+
this.acceptsSteering = false;
|
|
1622
|
+
if (this.aborted || sawTurnInterrupted) {
|
|
1623
|
+
const cost2 = this.buildCostData(this.accumulatedUsage, true);
|
|
1624
|
+
this.emit({ type: "result", cost: cost2, isError: true, errorCategory: "cancelled" });
|
|
1625
|
+
this.settle({
|
|
1626
|
+
exitCode: 130,
|
|
1627
|
+
sessionId: this._sessionId,
|
|
1628
|
+
cost: cost2,
|
|
1629
|
+
isError: true,
|
|
1630
|
+
failureReason: this.aborted ? this.abortReason : "Codex turn interrupted"
|
|
1631
|
+
});
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const isError = Boolean(terminalError) || !sawTurnCompleted;
|
|
1635
|
+
const cost = this.buildCostData(this.accumulatedUsage, isError);
|
|
1636
|
+
this.emit({
|
|
1637
|
+
type: "result",
|
|
1638
|
+
cost,
|
|
1639
|
+
isError,
|
|
1640
|
+
errorCategory: terminalError ? terminalError.category ?? "turn_failed" : undefined
|
|
1641
|
+
});
|
|
1642
|
+
this.settle({
|
|
1643
|
+
exitCode: isError ? 1 : 0,
|
|
1644
|
+
sessionId: this._sessionId,
|
|
1645
|
+
cost,
|
|
1646
|
+
isError,
|
|
1647
|
+
failureReason: terminalError?.message,
|
|
1648
|
+
rateLimitResetAt: this.errorTracker.getRateLimitResetAt(),
|
|
1649
|
+
rateLimitWindows: this.errorTracker.getRateLimitWindows()
|
|
1650
|
+
});
|
|
1651
|
+
} catch (err) {
|
|
1652
|
+
this.acceptsSteering = false;
|
|
1653
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1654
|
+
this.emit({ type: "raw_stderr", content: `[codex] Error: ${message}
|
|
1655
|
+
` });
|
|
1656
|
+
this.emit({ type: "error", message });
|
|
1657
|
+
const cost = this.buildCostData(this.accumulatedUsage, true);
|
|
1658
|
+
this.emit({ type: "result", cost, isError: true, errorCategory: "exception" });
|
|
1659
|
+
this.settle({
|
|
1660
|
+
exitCode: 1,
|
|
1661
|
+
sessionId: this._sessionId,
|
|
1662
|
+
cost,
|
|
1663
|
+
isError: true,
|
|
1664
|
+
failureReason: message,
|
|
1665
|
+
rateLimitResetAt: this.errorTracker.getRateLimitResetAt(),
|
|
1666
|
+
rateLimitWindows: this.errorTracker.getRateLimitWindows()
|
|
1667
|
+
});
|
|
1668
|
+
} finally {
|
|
1669
|
+
const sessionWasAborted = this.aborted || this.abortController?.signal.aborted === true || this.pendingResult?.exitCode === 130;
|
|
1670
|
+
if (process.env.SKIP_SESSION_SUMMARY !== "1" && !sessionWasAborted) {
|
|
1671
|
+
try {
|
|
1672
|
+
await this.summarizeAtEnd();
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
console.error("session_summary failed (codex):", err);
|
|
1675
|
+
}
|
|
1676
|
+
} else if (sessionWasAborted) {
|
|
1677
|
+
console.debug("[codex] session aborted — skipping session_summary");
|
|
1678
|
+
} else {
|
|
1679
|
+
console.debug("session_summary skipped (codex): SKIP_SESSION_SUMMARY is set");
|
|
1680
|
+
}
|
|
1681
|
+
this.abortRef.current = null;
|
|
1682
|
+
try {
|
|
1683
|
+
await this.thread.close();
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
this.emit({
|
|
1686
|
+
type: "raw_stderr",
|
|
1687
|
+
content: `[codex] Failed to close app-server: ${String(error)}
|
|
1688
|
+
`
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
try {
|
|
1692
|
+
await this.logFileHandle.end();
|
|
1693
|
+
} catch {}
|
|
1694
|
+
await this.agentsMdHandle.cleanup();
|
|
1695
|
+
const finalResult = this.pendingResult ?? {
|
|
1696
|
+
exitCode: 1,
|
|
1697
|
+
sessionId: this._sessionId,
|
|
1698
|
+
cost: this.buildCostData(this.accumulatedUsage, true),
|
|
1699
|
+
isError: true,
|
|
1700
|
+
failureReason: "session did not settle"
|
|
1701
|
+
};
|
|
1702
|
+
this.resolveCompletion(finalResult);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
async summarizeAtEnd() {
|
|
1706
|
+
const transcriptStr = this.transcript.join(`
|
|
1707
|
+
`).slice(-20000);
|
|
1708
|
+
const { agentId, taskId, apiUrl, apiKey } = this.config;
|
|
1709
|
+
if (!agentId || !taskId || !apiUrl || !apiKey) {
|
|
1710
|
+
const missing = [
|
|
1711
|
+
!agentId && "agent id",
|
|
1712
|
+
!taskId && "task id",
|
|
1713
|
+
!apiUrl && "API URL",
|
|
1714
|
+
!apiKey && "API key"
|
|
1715
|
+
].filter(Boolean);
|
|
1716
|
+
console.warn(`session_summary skipped (codex): missing ${missing.join(", ")}`);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (transcriptStr.length <= 100) {
|
|
1720
|
+
console.warn(`session_summary skipped (codex): transcript too short (${transcriptStr.length} chars)`);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const _runSummarize = this.summarizeDeps.runSummarize ?? summarizeSession;
|
|
1724
|
+
const _fetchRetrievals = this.summarizeDeps.fetchRetrievalsForTask ?? fetchRetrievalsForTask;
|
|
1725
|
+
const _postRatings = this.summarizeDeps.postRatings ?? postRatings;
|
|
1726
|
+
const _buildRatings = this.summarizeDeps.buildRatingsFromLlm ?? buildRatingsFromLlm;
|
|
1727
|
+
const wantRatings = getMemoryRaterNames().includes("llm");
|
|
1728
|
+
const retrievals = wantRatings ? await _fetchRetrievals({ apiUrl, apiKey, agentId, taskId }).catch(() => []) : [];
|
|
1729
|
+
const result = await _runSummarize({
|
|
1730
|
+
harness: "codex",
|
|
1731
|
+
transcript: transcriptStr,
|
|
1732
|
+
retrievals,
|
|
1733
|
+
taskContext: {
|
|
1734
|
+
sourceTaskId: taskId,
|
|
1735
|
+
agentId,
|
|
1736
|
+
prompt: this.config.prompt
|
|
1737
|
+
},
|
|
1738
|
+
apiUrl,
|
|
1739
|
+
apiKey,
|
|
1740
|
+
env: this.config.env
|
|
1741
|
+
});
|
|
1742
|
+
if (!result) {
|
|
1743
|
+
console.warn("session_summary skipped (codex): summarizer returned no result");
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const summary = result.summary.trim();
|
|
1747
|
+
if (summary.length <= 20 || summary.toLowerCase().includes("no significant learnings")) {
|
|
1748
|
+
console.debug(`session_summary skipped (codex): summary failed quality gate (${summary.length} chars)`);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const indexResp = await fetch(`${apiUrl}/api/memory/index`, {
|
|
1752
|
+
method: "POST",
|
|
1753
|
+
headers: {
|
|
1754
|
+
"Content-Type": "application/json",
|
|
1755
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1756
|
+
"X-Agent-ID": agentId
|
|
1757
|
+
},
|
|
1758
|
+
body: JSON.stringify({
|
|
1759
|
+
scope: "agent",
|
|
1760
|
+
source: "session_summary",
|
|
1761
|
+
sourceTaskId: taskId,
|
|
1762
|
+
content: summary,
|
|
1763
|
+
name: "session-summary",
|
|
1764
|
+
agentId
|
|
1765
|
+
})
|
|
1766
|
+
});
|
|
1767
|
+
if (!indexResp.ok) {
|
|
1768
|
+
const text = await indexResp.text().catch(() => "");
|
|
1769
|
+
console.error("session_summary: /api/memory/index POST failed (codex):", indexResp.status, text);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (wantRatings && result.ratings && result.ratings.length > 0) {
|
|
1773
|
+
const ratingEvents = _buildRatings(result.ratings, retrievals);
|
|
1774
|
+
if (ratingEvents.length > 0) {
|
|
1775
|
+
await _postRatings({
|
|
1776
|
+
apiUrl,
|
|
1777
|
+
apiKey,
|
|
1778
|
+
agentId,
|
|
1779
|
+
taskId,
|
|
1780
|
+
events: ratingEvents
|
|
1781
|
+
}).catch((err) => console.error("session_summary: postRatings failed (codex):", err));
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async function createInProcessCodexSession(config, opts = {}) {
|
|
1787
|
+
const agentsMdHandle = await writeCodexAgentsMd(config.cwd, config.systemPrompt);
|
|
1788
|
+
try {
|
|
1789
|
+
const resolvedModel = resolveCodexModel(config.model);
|
|
1790
|
+
const preSessionEvents = [];
|
|
1791
|
+
const bufferedEmit = (event) => {
|
|
1792
|
+
preSessionEvents.push(event);
|
|
1793
|
+
};
|
|
1794
|
+
const mergedConfig = await buildCodexConfig(config, resolvedModel, bufferedEmit);
|
|
1795
|
+
const authMode = await resolveCodexAuthMode(config, bufferedEmit);
|
|
1796
|
+
const env = {
|
|
1797
|
+
PATH: process.env.PATH ?? "",
|
|
1798
|
+
HOME: process.env.HOME ?? "",
|
|
1799
|
+
...authMode !== "chatgpt" && process.env.OPENAI_API_KEY ? { OPENAI_API_KEY: process.env.OPENAI_API_KEY } : {},
|
|
1800
|
+
...process.env.NODE_EXTRA_CA_CERTS ? { NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS } : {},
|
|
1801
|
+
CONTEXT_MODE_EXTERNAL_MCP_NUDGE_EVERY: CTX_MODE_NUDGE_EVERY,
|
|
1802
|
+
...config.env ?? {},
|
|
1803
|
+
...buildOtelTraceparentEnv(config.env ?? process.env),
|
|
1804
|
+
SWARM_CODEX_APP_SERVER: "1"
|
|
1805
|
+
};
|
|
1806
|
+
if (authMode === "chatgpt")
|
|
1807
|
+
delete env.OPENAI_API_KEY;
|
|
1808
|
+
const codexPathOverride = process.env.CODEX_PATH_OVERRIDE;
|
|
1809
|
+
if (config.resumeSessionId) {
|
|
1810
|
+
console.warn("[codex-adapter] resumeSessionId ignored — native resume is disabled by deprecation plan");
|
|
1811
|
+
}
|
|
1812
|
+
let thread;
|
|
1813
|
+
let liveSession;
|
|
1814
|
+
if (opts.threadFactory) {
|
|
1815
|
+
thread = await opts.threadFactory({
|
|
1816
|
+
config,
|
|
1817
|
+
resolvedModel,
|
|
1818
|
+
env,
|
|
1819
|
+
codexConfig: mergedConfig
|
|
1820
|
+
});
|
|
1821
|
+
} else {
|
|
1822
|
+
const appServerFactory = opts.appServerFactory ?? ((options) => new CodexAppServer(options));
|
|
1823
|
+
const client = appServerFactory({
|
|
1824
|
+
...codexPathOverride ? { codexPath: codexPathOverride } : {},
|
|
1825
|
+
env,
|
|
1826
|
+
onStderr: (line) => {
|
|
1827
|
+
if (liveSession)
|
|
1828
|
+
liveSession.emitTransportStderr(line);
|
|
1829
|
+
else
|
|
1830
|
+
bufferedEmit({ type: "raw_stderr", content: `${line}
|
|
1831
|
+
` });
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
try {
|
|
1835
|
+
const response = await client.request("thread/start", {
|
|
1836
|
+
model: resolvedModel,
|
|
1837
|
+
cwd: config.cwd,
|
|
1838
|
+
approvalPolicy: "never",
|
|
1839
|
+
sandbox: "danger-full-access",
|
|
1840
|
+
config: mergedConfig,
|
|
1841
|
+
ephemeral: true
|
|
1842
|
+
});
|
|
1843
|
+
thread = new AppServerCodexThread(client, response.thread.id);
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
await client.close();
|
|
1846
|
+
throw error;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
const session = new CodexSession(thread, config, agentsMdHandle, resolvedModel, preSessionEvents, opts.skillsDir, opts.summarizeDeps ?? {});
|
|
1850
|
+
liveSession = session;
|
|
1851
|
+
return session;
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
await agentsMdHandle.cleanup();
|
|
1854
|
+
throw err;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function resolveCodexRunnerArgv() {
|
|
1858
|
+
const override = process.env.AGENT_SWARM_CODEX_RUNNER_ARGV;
|
|
1859
|
+
if (override) {
|
|
1860
|
+
try {
|
|
1861
|
+
const parsed = JSON.parse(override);
|
|
1862
|
+
if (Array.isArray(parsed) && parsed.every((s) => typeof s === "string")) {
|
|
1863
|
+
return parsed;
|
|
1864
|
+
}
|
|
1865
|
+
} catch {}
|
|
1866
|
+
}
|
|
1867
|
+
const execPath = process.execPath;
|
|
1868
|
+
const scriptArg = process.argv[1];
|
|
1869
|
+
if (scriptArg && /\.(t|j)sx?$/.test(scriptArg)) {
|
|
1870
|
+
return [execPath, scriptArg, "codex-session-runner"];
|
|
1871
|
+
}
|
|
1872
|
+
return [execPath, "codex-session-runner"];
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
class CodexSubprocessSession {
|
|
1876
|
+
proc;
|
|
1877
|
+
listeners = [];
|
|
1878
|
+
eventQueue = [];
|
|
1879
|
+
completionPromise;
|
|
1880
|
+
_sessionId;
|
|
1881
|
+
finished = false;
|
|
1882
|
+
nextControlId = 0;
|
|
1883
|
+
pendingSteering = new Map;
|
|
1884
|
+
constructor(config, skillsDir) {
|
|
1885
|
+
const argv = resolveCodexRunnerArgv();
|
|
1886
|
+
const payload = {
|
|
1887
|
+
config,
|
|
1888
|
+
skillsDir,
|
|
1889
|
+
parentOtelEnv: buildOtelTraceparentEnv(config.env ?? process.env)
|
|
1890
|
+
};
|
|
1891
|
+
const apiKey = getApiKey();
|
|
1892
|
+
this.proc = registerProcessGroup(Bun.spawn(argv, {
|
|
1893
|
+
detached: detachedProcessGroup,
|
|
1894
|
+
env: {
|
|
1895
|
+
PATH: process.env.PATH ?? "",
|
|
1896
|
+
HOME: process.env.HOME ?? "",
|
|
1897
|
+
...process.env.NODE_EXTRA_CA_CERTS ? { NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS } : {},
|
|
1898
|
+
...process.env.MCP_BASE_URL ? { MCP_BASE_URL: process.env.MCP_BASE_URL } : {},
|
|
1899
|
+
...apiKey ? { AGENT_SWARM_API_KEY: apiKey, API_KEY: apiKey } : {},
|
|
1900
|
+
...process.env.OPENAI_API_KEY ? { OPENAI_API_KEY: process.env.OPENAI_API_KEY } : {},
|
|
1901
|
+
...process.env.OPENROUTER_API_KEY ? { OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY } : {},
|
|
1902
|
+
...process.env.ANTHROPIC_API_KEY ? { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY } : {},
|
|
1903
|
+
...process.env.CODEX_PATH_OVERRIDE ? { CODEX_PATH_OVERRIDE: process.env.CODEX_PATH_OVERRIDE } : {},
|
|
1904
|
+
...process.env.CODEX_SKILLS_DIR ? { CODEX_SKILLS_DIR: process.env.CODEX_SKILLS_DIR } : {},
|
|
1905
|
+
CONTEXT_MODE_EXTERNAL_MCP_NUDGE_EVERY: CTX_MODE_NUDGE_EVERY,
|
|
1906
|
+
...process.env.SKIP_SESSION_SUMMARY ? { SKIP_SESSION_SUMMARY: process.env.SKIP_SESSION_SUMMARY } : {},
|
|
1907
|
+
MEMORY_RATERS: getMemoryRaterNames().join(",")
|
|
1908
|
+
},
|
|
1909
|
+
stdin: "pipe",
|
|
1910
|
+
stdout: "pipe",
|
|
1911
|
+
stderr: "pipe"
|
|
1912
|
+
}));
|
|
1913
|
+
const stdin = this.proc.stdin;
|
|
1914
|
+
stdin.write(`${JSON.stringify(payload)}
|
|
1915
|
+
`);
|
|
1916
|
+
this.completionPromise = this.processStreams().finally(() => this.finishSteering());
|
|
1917
|
+
}
|
|
1918
|
+
get sessionId() {
|
|
1919
|
+
return this._sessionId;
|
|
1920
|
+
}
|
|
1921
|
+
onEvent(listener) {
|
|
1922
|
+
this.listeners.push(listener);
|
|
1923
|
+
for (const event of this.eventQueue) {
|
|
1924
|
+
listener(event);
|
|
1925
|
+
}
|
|
1926
|
+
this.eventQueue.length = 0;
|
|
1927
|
+
}
|
|
1928
|
+
async waitForCompletion() {
|
|
1929
|
+
return this.completionPromise;
|
|
1930
|
+
}
|
|
1931
|
+
async abort(reason) {
|
|
1932
|
+
if (this.finished)
|
|
1933
|
+
return;
|
|
1934
|
+
try {
|
|
1935
|
+
this.writeControl({ kind: "abort", reason });
|
|
1936
|
+
let timer;
|
|
1937
|
+
try {
|
|
1938
|
+
await Promise.race([
|
|
1939
|
+
this.completionPromise,
|
|
1940
|
+
new Promise((resolve2) => {
|
|
1941
|
+
timer = setTimeout(resolve2, 2000);
|
|
1942
|
+
})
|
|
1943
|
+
]);
|
|
1944
|
+
} finally {
|
|
1945
|
+
clearTimeout(timer);
|
|
1946
|
+
}
|
|
1947
|
+
} finally {
|
|
1948
|
+
if (!this.finished)
|
|
1949
|
+
await terminateProcessGroup(this.proc.pid);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
async deliverSteering(delivery) {
|
|
1953
|
+
if (this.finished)
|
|
1954
|
+
return { delivered: false, reason: "Codex session has completed" };
|
|
1955
|
+
const id = ++this.nextControlId;
|
|
1956
|
+
return new Promise((resolve2) => {
|
|
1957
|
+
const timer = delivery.mode === "queue" ? undefined : setTimeout(() => {
|
|
1958
|
+
this.pendingSteering.delete(id);
|
|
1959
|
+
resolve2({ delivered: false, reason: "Codex steering request timed out" });
|
|
1960
|
+
}, 35000);
|
|
1961
|
+
this.pendingSteering.set(id, { resolve: resolve2, timer });
|
|
1962
|
+
try {
|
|
1963
|
+
this.writeControl({ kind: "steer", id, delivery });
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
clearTimeout(timer);
|
|
1966
|
+
this.pendingSteering.delete(id);
|
|
1967
|
+
resolve2({ delivered: false, reason: scrubSecrets(String(error)) });
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
writeControl(command) {
|
|
1972
|
+
this.proc.stdin.write(`${JSON.stringify(command)}
|
|
1973
|
+
`);
|
|
1974
|
+
}
|
|
1975
|
+
finishSteering() {
|
|
1976
|
+
this.finished = true;
|
|
1977
|
+
for (const { resolve: resolve2, timer } of this.pendingSteering.values()) {
|
|
1978
|
+
clearTimeout(timer);
|
|
1979
|
+
resolve2({ delivered: false, reason: "Codex session has completed" });
|
|
1980
|
+
}
|
|
1981
|
+
this.pendingSteering.clear();
|
|
1982
|
+
}
|
|
1983
|
+
emit(event) {
|
|
1984
|
+
event = JSON.parse(scrubSecrets(JSON.stringify(event)));
|
|
1985
|
+
if (event.type === "session_init" && event.sessionId) {
|
|
1986
|
+
this._sessionId = event.sessionId;
|
|
1987
|
+
}
|
|
1988
|
+
if (this.listeners.length > 0) {
|
|
1989
|
+
for (const listener of this.listeners) {
|
|
1990
|
+
try {
|
|
1991
|
+
listener(event);
|
|
1992
|
+
} catch {}
|
|
1993
|
+
}
|
|
1994
|
+
} else {
|
|
1995
|
+
this.eventQueue.push(event);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async processStreams() {
|
|
1999
|
+
let result = null;
|
|
2000
|
+
let partial = "";
|
|
2001
|
+
let stderrTail = "";
|
|
2002
|
+
const stdoutPromise = (async () => {
|
|
2003
|
+
const stdout = this.proc.stdout;
|
|
2004
|
+
if (!stdout)
|
|
2005
|
+
return;
|
|
2006
|
+
const decoder = new TextDecoder;
|
|
2007
|
+
for await (const chunk of stdout) {
|
|
2008
|
+
partial += decoder.decode(chunk, { stream: true });
|
|
2009
|
+
const parts = partial.split(`
|
|
2010
|
+
`);
|
|
2011
|
+
partial = parts.pop() ?? "";
|
|
2012
|
+
for (const line of parts) {
|
|
2013
|
+
const trimmed = line.trim();
|
|
2014
|
+
if (!trimmed)
|
|
2015
|
+
continue;
|
|
2016
|
+
this.handleLine(trimmed, (r) => {
|
|
2017
|
+
result = r;
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
partial += decoder.decode();
|
|
2022
|
+
if (partial.trim()) {
|
|
2023
|
+
this.handleLine(partial.trim(), (r) => {
|
|
2024
|
+
result = r;
|
|
2025
|
+
});
|
|
2026
|
+
partial = "";
|
|
2027
|
+
}
|
|
2028
|
+
})();
|
|
2029
|
+
const stderrPromise = (async () => {
|
|
2030
|
+
const stderr = this.proc.stderr;
|
|
2031
|
+
if (!stderr)
|
|
2032
|
+
return;
|
|
2033
|
+
const decoder = new TextDecoder;
|
|
2034
|
+
let pending = "";
|
|
2035
|
+
const emitStderr = (line) => {
|
|
2036
|
+
const text = scrubSecrets(line);
|
|
2037
|
+
stderrTail = (stderrTail + text).slice(-2000);
|
|
2038
|
+
this.emit({ type: "raw_stderr", content: text });
|
|
2039
|
+
};
|
|
2040
|
+
for await (const chunk of stderr) {
|
|
2041
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
2042
|
+
let newline = pending.indexOf(`
|
|
2043
|
+
`);
|
|
2044
|
+
while (newline !== -1) {
|
|
2045
|
+
emitStderr(pending.slice(0, newline + 1));
|
|
2046
|
+
pending = pending.slice(newline + 1);
|
|
2047
|
+
newline = pending.indexOf(`
|
|
2048
|
+
`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
pending += decoder.decode();
|
|
2052
|
+
if (pending)
|
|
2053
|
+
emitStderr(pending);
|
|
2054
|
+
})();
|
|
2055
|
+
await Promise.all([stdoutPromise, stderrPromise]);
|
|
2056
|
+
const exitCode = await this.proc.exited;
|
|
2057
|
+
this.finishSteering();
|
|
2058
|
+
await terminateProcessGroup(this.proc.pid);
|
|
2059
|
+
if (result) {
|
|
2060
|
+
return result;
|
|
2061
|
+
}
|
|
2062
|
+
const stderrHint = stderrTail.trim() ? `: stderr: ${stderrTail.trim().slice(-500)}` : "";
|
|
2063
|
+
return {
|
|
2064
|
+
exitCode: exitCode ?? 1,
|
|
2065
|
+
sessionId: this._sessionId,
|
|
2066
|
+
isError: true,
|
|
2067
|
+
failureReason: `codex subprocess exited (code=${exitCode ?? "?"}) without a structured result${stderrHint}`
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
handleLine(line, setResult) {
|
|
2071
|
+
let msg;
|
|
2072
|
+
try {
|
|
2073
|
+
msg = JSON.parse(scrubSecrets(line));
|
|
2074
|
+
} catch {
|
|
2075
|
+
this.emit({ type: "raw_stderr", content: `${line}
|
|
2076
|
+
` });
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (msg.kind === "event" && msg.event) {
|
|
2080
|
+
this.emit(msg.event);
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (msg.kind === "result" && msg.result) {
|
|
2084
|
+
this.finishSteering();
|
|
2085
|
+
setResult(msg.result);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (msg.kind === "steering-result" && msg.id !== undefined && msg.delivery) {
|
|
2089
|
+
const pending = this.pendingSteering.get(msg.id);
|
|
2090
|
+
if (pending) {
|
|
2091
|
+
clearTimeout(pending.timer);
|
|
2092
|
+
this.pendingSteering.delete(msg.id);
|
|
2093
|
+
pending.resolve(msg.delivery);
|
|
2094
|
+
}
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
if (msg.kind === "error" && msg.message) {
|
|
2098
|
+
this.emit({ type: "error", message: msg.message });
|
|
2099
|
+
setResult({
|
|
2100
|
+
exitCode: 1,
|
|
2101
|
+
sessionId: this._sessionId,
|
|
2102
|
+
isError: true,
|
|
2103
|
+
failureReason: msg.message
|
|
2104
|
+
});
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
class CodexAdapter {
|
|
2111
|
+
name = "codex";
|
|
2112
|
+
traits = {
|
|
2113
|
+
hasMcp: true,
|
|
2114
|
+
nativeSkillDiscovery: false,
|
|
2115
|
+
hasLocalEnvironment: true,
|
|
2116
|
+
steerModes: ["steer", "queue"]
|
|
2117
|
+
};
|
|
2118
|
+
skillsDir;
|
|
2119
|
+
summarizeDeps;
|
|
2120
|
+
appServerFactory;
|
|
2121
|
+
threadFactory;
|
|
2122
|
+
bypassSubprocess;
|
|
2123
|
+
constructor(opts = {}) {
|
|
2124
|
+
this.skillsDir = opts.skillsDir;
|
|
2125
|
+
this.summarizeDeps = opts.summarizeDeps ?? {};
|
|
2126
|
+
this.bypassSubprocess = opts.bypassSubprocess ?? false;
|
|
2127
|
+
this.appServerFactory = opts.appServerFactory;
|
|
2128
|
+
this.threadFactory = opts.threadFactory;
|
|
2129
|
+
}
|
|
2130
|
+
async createSession(config) {
|
|
2131
|
+
if (this.bypassSubprocess) {
|
|
2132
|
+
return createInProcessCodexSession(config, {
|
|
2133
|
+
skillsDir: this.skillsDir,
|
|
2134
|
+
summarizeDeps: this.summarizeDeps,
|
|
2135
|
+
appServerFactory: this.appServerFactory,
|
|
2136
|
+
threadFactory: this.threadFactory
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
return new CodexSubprocessSession(config, this.skillsDir);
|
|
2140
|
+
}
|
|
2141
|
+
async canResume(_sessionId) {
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
formatCommand(commandName) {
|
|
2145
|
+
return `/${commandName}`;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
export { codexUserHome, checkCodexCredentials, resolveCodexAuthMode, buildCodexConfig, CodexSession, createInProcessCodexSession, resolveCodexRunnerArgv, CodexAdapter };
|