@xdelivered/emberflow 0.2.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/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { AgentEvent } from './types';
|
|
2
|
+
|
|
3
|
+
interface ClaudeToolResultTextBlock {
|
|
4
|
+
type: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ClaudeContentBlock {
|
|
9
|
+
type: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
input?: Record<string, unknown>;
|
|
13
|
+
id?: string; // tool_use id (assistant tool_use blocks)
|
|
14
|
+
tool_use_id?: string; // correlates a tool_result back to its tool_use
|
|
15
|
+
content?: string | ClaudeToolResultTextBlock[]; // tool_result payload
|
|
16
|
+
is_error?: boolean; // tool_result failure flag
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ClaudeLine {
|
|
20
|
+
type: string;
|
|
21
|
+
subtype?: string;
|
|
22
|
+
is_error?: boolean;
|
|
23
|
+
message?: {
|
|
24
|
+
content?: ClaudeContentBlock[];
|
|
25
|
+
};
|
|
26
|
+
result?: string;
|
|
27
|
+
usage?: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Best-effort extraction of a shell command from a Bash tool_use input. */
|
|
31
|
+
function bashCommandFrom(block: ClaudeContentBlock): string | undefined {
|
|
32
|
+
const input = block.input;
|
|
33
|
+
if (!input) return undefined;
|
|
34
|
+
const command = input.command;
|
|
35
|
+
return typeof command === 'string' ? command : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Fallback label for a tool whose input we don't format specially (or that's
|
|
39
|
+
* missing its expected field): `<name> <compact-json>` capped at 80 chars. */
|
|
40
|
+
function compactLabel(name: string, input: Record<string, unknown>): string {
|
|
41
|
+
const json = JSON.stringify(input ?? {});
|
|
42
|
+
return `${name} ${json.length > 80 ? json.slice(0, 80) : json}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Human label for a non-mcp tool_use block, used as a `command` event's
|
|
47
|
+
* `command` string so Read/Edit/Grep/… fold into the UI's "Ran N commands"
|
|
48
|
+
* group instead of leaking as reasoning prose. Bash keeps its raw shell command;
|
|
49
|
+
* the common Claude tools get a purpose-built one-liner; anything else gets the
|
|
50
|
+
* compact `<name> <json>` fallback.
|
|
51
|
+
*/
|
|
52
|
+
function commandLabelFor(block: ClaudeContentBlock): string {
|
|
53
|
+
const name = block.name ?? 'tool';
|
|
54
|
+
const input = (block.input ?? {}) as Record<string, unknown>;
|
|
55
|
+
const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
56
|
+
|
|
57
|
+
switch (name) {
|
|
58
|
+
case 'Bash':
|
|
59
|
+
return bashCommandFrom(block) ?? compactLabel(name, input);
|
|
60
|
+
case 'Read': {
|
|
61
|
+
const fp = str(input.file_path);
|
|
62
|
+
if (!fp) return compactLabel(name, input);
|
|
63
|
+
const bits: string[] = [];
|
|
64
|
+
if (typeof input.offset === 'number') bits.push(`offset ${input.offset}`);
|
|
65
|
+
if (typeof input.limit === 'number') bits.push(`limit ${input.limit}`);
|
|
66
|
+
return `Read ${fp}${bits.length ? ` (${bits.join(', ')})` : ''}`;
|
|
67
|
+
}
|
|
68
|
+
case 'Edit':
|
|
69
|
+
case 'Write': {
|
|
70
|
+
const fp = str(input.file_path);
|
|
71
|
+
return fp ? `${name} ${fp}` : compactLabel(name, input);
|
|
72
|
+
}
|
|
73
|
+
case 'Grep': {
|
|
74
|
+
const pattern = str(input.pattern);
|
|
75
|
+
if (!pattern) return compactLabel(name, input);
|
|
76
|
+
const path = str(input.path);
|
|
77
|
+
return `Grep ${pattern}${path ? ` ${path}` : ''}`;
|
|
78
|
+
}
|
|
79
|
+
case 'Glob': {
|
|
80
|
+
const pattern = str(input.pattern);
|
|
81
|
+
return pattern ? `Glob ${pattern}` : compactLabel(name, input);
|
|
82
|
+
}
|
|
83
|
+
case 'TodoWrite':
|
|
84
|
+
return 'update todos';
|
|
85
|
+
default:
|
|
86
|
+
return compactLabel(name, input);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Flattens a tool_result's `content` (string, or array of text blocks) to plain text. */
|
|
91
|
+
function toolResultTextFrom(block: ClaudeContentBlock): string {
|
|
92
|
+
const content = block.content;
|
|
93
|
+
if (typeof content === 'string') return content;
|
|
94
|
+
if (Array.isArray(content)) {
|
|
95
|
+
return content
|
|
96
|
+
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
97
|
+
.map((c) => c.text)
|
|
98
|
+
.join('');
|
|
99
|
+
}
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function parseClaudeLine(line: string): AgentEvent[] {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
if (!trimmed) return [];
|
|
106
|
+
|
|
107
|
+
let parsed: ClaudeLine;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(trimmed);
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
switch (parsed.type) {
|
|
115
|
+
case 'system': {
|
|
116
|
+
if (parsed.subtype === 'init') return [{ type: 'started' }];
|
|
117
|
+
// hook_started / hook_response and any other system noise — ignore.
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'assistant': {
|
|
122
|
+
const blocks = parsed.message?.content ?? [];
|
|
123
|
+
// Claude routinely emits multiple content blocks per message (e.g.
|
|
124
|
+
// explain-then-call `[text, tool_use]`, or parallel tool calls) — emit
|
|
125
|
+
// one AgentEvent per meaningful block, in order, so the console shows the
|
|
126
|
+
// full picture. `thinking` and other block types are skipped.
|
|
127
|
+
const events: AgentEvent[] = [];
|
|
128
|
+
for (const block of blocks) {
|
|
129
|
+
if (block.type === 'text' && block.text) {
|
|
130
|
+
events.push({ type: 'message', text: block.text });
|
|
131
|
+
} else if (block.type === 'tool_use') {
|
|
132
|
+
// MCP tools surface as `mcp__<server>__<tool>` — show them as MCP calls.
|
|
133
|
+
if (block.name?.startsWith('mcp__')) {
|
|
134
|
+
const [, server, ...toolParts] = block.name.split('__');
|
|
135
|
+
events.push({
|
|
136
|
+
type: 'mcp',
|
|
137
|
+
mcpServer: server || 'mcp',
|
|
138
|
+
mcpTool: toolParts.join('__') || 'tool',
|
|
139
|
+
mcpStatus: 'in_progress',
|
|
140
|
+
...(block.id ? { toolUseId: block.id } : {}),
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Every other tool (Bash, Read, Edit, Grep, Glob, TodoWrite, …) is a
|
|
145
|
+
// `command` event with a human label, correlated by tool_use_id like
|
|
146
|
+
// Bash always was — so it folds into the UI's "Ran N commands" group
|
|
147
|
+
// instead of leaking as reasoning prose.
|
|
148
|
+
events.push({
|
|
149
|
+
type: 'command',
|
|
150
|
+
command: commandLabelFor(block),
|
|
151
|
+
commandStatus: 'in_progress',
|
|
152
|
+
...(block.id ? { toolUseId: block.id } : {}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return events;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'user': {
|
|
160
|
+
// Tool results. Emit a partial completion carrying the tool_use_id +
|
|
161
|
+
// output text + status; the adapter looks up the original command text
|
|
162
|
+
// by id (it isn't repeated in the tool_result line) and skips gracefully
|
|
163
|
+
// if the id doesn't correspond to a command it's tracking (e.g. a
|
|
164
|
+
// non-Bash tool result).
|
|
165
|
+
const blocks = parsed.message?.content ?? [];
|
|
166
|
+
const events: AgentEvent[] = [];
|
|
167
|
+
for (const block of blocks) {
|
|
168
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
169
|
+
events.push({
|
|
170
|
+
type: 'command',
|
|
171
|
+
toolUseId: block.tool_use_id,
|
|
172
|
+
commandStatus: block.is_error ? 'failed' : 'completed',
|
|
173
|
+
output: toolResultTextFrom(block),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return events;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'result': {
|
|
181
|
+
if (parsed.subtype === 'error' || parsed.is_error) {
|
|
182
|
+
return [{ type: 'error', text: parsed.result ?? 'error' }];
|
|
183
|
+
}
|
|
184
|
+
return [{ type: 'done', usage: parsed.usage }];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case 'rate_limit_event':
|
|
188
|
+
return [];
|
|
189
|
+
|
|
190
|
+
default:
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawnCodex } from './codexAdapter';
|
|
6
|
+
import type { AgentEvent } from './types';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const fakeCodexPath = path.join(__dirname, '__fixtures__', 'fake-codex.mjs');
|
|
10
|
+
|
|
11
|
+
async function collect(events: AsyncIterable<AgentEvent>): Promise<AgentEvent[]> {
|
|
12
|
+
const out: AgentEvent[] = [];
|
|
13
|
+
for await (const e of events) {
|
|
14
|
+
out.push(e);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('spawnCodex', () => {
|
|
20
|
+
it('normalizes fake-codex stdout into [started, command, message, done]', async () => {
|
|
21
|
+
const agent = spawnCodex('do the thing', os.tmpdir(), { bin: fakeCodexPath });
|
|
22
|
+
const events = await collect(agent.events);
|
|
23
|
+
expect(events).toEqual([
|
|
24
|
+
{ type: 'started' },
|
|
25
|
+
{ type: 'command', command: 'ls', commandStatus: 'in_progress' },
|
|
26
|
+
{ type: 'message', text: 'Done listing files' },
|
|
27
|
+
{ type: 'done', usage: { output_tokens: 5 } },
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('yields a terminal error event when the process exits non-zero without a done event', async () => {
|
|
32
|
+
process.env.FAKE_CODEX_FAIL = '1';
|
|
33
|
+
try {
|
|
34
|
+
const agent = spawnCodex('do the thing', os.tmpdir(), { bin: fakeCodexPath });
|
|
35
|
+
const events = await collect(agent.events);
|
|
36
|
+
expect(events[0]).toEqual({ type: 'started' });
|
|
37
|
+
expect(events[1]).toEqual({ type: 'command', command: 'ls', commandStatus: 'in_progress' });
|
|
38
|
+
expect(events[2]).toEqual({ type: 'message', text: 'Done listing files' });
|
|
39
|
+
const last = events[events.length - 1];
|
|
40
|
+
expect(last.type).toBe('error');
|
|
41
|
+
expect(last.text).not.toContain('hint:');
|
|
42
|
+
} finally {
|
|
43
|
+
delete process.env.FAKE_CODEX_FAIL;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('appends an actionable hint to the terminal error when stderr looks like a model-rejection', async () => {
|
|
48
|
+
process.env.FAKE_CODEX_MODEL_REJECT = '1';
|
|
49
|
+
try {
|
|
50
|
+
const agent = spawnCodex('do the thing', os.tmpdir(), { bin: fakeCodexPath });
|
|
51
|
+
const events = await collect(agent.events);
|
|
52
|
+
const last = events[events.length - 1];
|
|
53
|
+
expect(last.type).toBe('error');
|
|
54
|
+
expect(last.text).toContain(
|
|
55
|
+
'hint: your codex CLI may be too old for the selected model — upgrade it or switch backend in Settings.',
|
|
56
|
+
);
|
|
57
|
+
} finally {
|
|
58
|
+
delete process.env.FAKE_CODEX_MODEL_REJECT;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('delivers every event including the trailing done when the child emits many lines then exits (no event-loss race)', async () => {
|
|
63
|
+
process.env.FAKE_CODEX_MANY = '1';
|
|
64
|
+
try {
|
|
65
|
+
const agent = spawnCodex('do the thing', os.tmpdir(), { bin: fakeCodexPath });
|
|
66
|
+
const events = await collect(agent.events);
|
|
67
|
+
// started + command + message + 27 filler messages + done = 31 events
|
|
68
|
+
expect(events.length).toBe(31);
|
|
69
|
+
expect(events[0]).toEqual({ type: 'started' });
|
|
70
|
+
expect(events[events.length - 1]).toEqual({ type: 'done', usage: { output_tokens: 5 } });
|
|
71
|
+
const fillers = events.filter((e) => e.type === 'message' && e.text?.startsWith('filler '));
|
|
72
|
+
expect(fillers.length).toBe(27);
|
|
73
|
+
} finally {
|
|
74
|
+
delete process.env.FAKE_CODEX_MANY;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('yields a terminal error (and does not crash) when the binary cannot be spawned', async () => {
|
|
79
|
+
const agent = spawnCodex('do the thing', os.tmpdir(), { bin: '/nonexistent/nope' });
|
|
80
|
+
const events = await collect(agent.events);
|
|
81
|
+
expect(events.length).toBeGreaterThan(0);
|
|
82
|
+
const last = events[events.length - 1];
|
|
83
|
+
expect(last.type).toBe('error');
|
|
84
|
+
expect(events.filter((e) => e.type === 'done')).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('cancel() sends SIGTERM and does not throw', async () => {
|
|
88
|
+
const agent = spawnCodex('do the thing', os.tmpdir(), { bin: fakeCodexPath });
|
|
89
|
+
expect(() => agent.cancel()).not.toThrow();
|
|
90
|
+
await collect(agent.events);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('cancel() escalates to SIGKILL if the process ignores SIGTERM', async () => {
|
|
94
|
+
process.env.FAKE_CODEX_HANG = '1';
|
|
95
|
+
try {
|
|
96
|
+
const agent = spawnCodex('do the thing', os.tmpdir(), { bin: fakeCodexPath, killGraceMs: 50 });
|
|
97
|
+
// Let the fixture install its SIGTERM handler before we cancel.
|
|
98
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
99
|
+
agent.cancel();
|
|
100
|
+
const events = await collect(agent.events);
|
|
101
|
+
const last = events[events.length - 1];
|
|
102
|
+
// SIGKILL can't be caught, so the process dies without emitting its
|
|
103
|
+
// own terminal event — the adapter must synthesize an error.
|
|
104
|
+
expect(last.type).toBe('error');
|
|
105
|
+
} finally {
|
|
106
|
+
delete process.env.FAKE_CODEX_HANG;
|
|
107
|
+
}
|
|
108
|
+
}, 10000);
|
|
109
|
+
|
|
110
|
+
it('synthesizes a terminal done when the process exits 0 without a turn.completed', async () => {
|
|
111
|
+
process.env.FAKE_CODEX_NODONE = '1';
|
|
112
|
+
try {
|
|
113
|
+
const agent = spawnCodex('do the thing', os.tmpdir(), { bin: fakeCodexPath });
|
|
114
|
+
const events = await collect(agent.events);
|
|
115
|
+
expect(events).toEqual([
|
|
116
|
+
{ type: 'started' },
|
|
117
|
+
{ type: 'command', command: 'ls', commandStatus: 'in_progress' },
|
|
118
|
+
{ type: 'message', text: 'Done listing files' },
|
|
119
|
+
{ type: 'done' },
|
|
120
|
+
]);
|
|
121
|
+
} finally {
|
|
122
|
+
delete process.env.FAKE_CODEX_NODONE;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { spawn, type ChildProcessByStdio } from 'node:child_process';
|
|
2
|
+
import type { Readable } from 'node:stream';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
import { parseCodexLine } from './codexParse';
|
|
5
|
+
import { modelRejectionHint } from './modelRejectionHint';
|
|
6
|
+
import type { AgentEvent } from './types';
|
|
7
|
+
|
|
8
|
+
const STDERR_TAIL_BYTES = 2048;
|
|
9
|
+
|
|
10
|
+
export interface SpawnedAgent {
|
|
11
|
+
events: AsyncIterable<AgentEvent>;
|
|
12
|
+
cancel(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CodexOptions {
|
|
16
|
+
model?: string;
|
|
17
|
+
reasoning?: 'low' | 'medium' | 'high';
|
|
18
|
+
bin?: string;
|
|
19
|
+
/** Grace period (ms) between SIGTERM and the SIGKILL escalation in cancel(). Test hook. */
|
|
20
|
+
killGraceMs?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function spawnCodex(prompt: string, projectDir: string, opts: CodexOptions = {}): SpawnedAgent {
|
|
24
|
+
const bin = opts.bin ?? 'codex';
|
|
25
|
+
const args = [
|
|
26
|
+
// Never ask for per-call approval — the studio runs codex non-interactively,
|
|
27
|
+
// so an approval prompt has no one to answer it and codex cancels the call
|
|
28
|
+
// ("user cancelled MCP tool call"). This is a top-level flag (before `exec`).
|
|
29
|
+
// The sandbox (-s workspace-write, below) still constrains what can run.
|
|
30
|
+
'-a',
|
|
31
|
+
'never',
|
|
32
|
+
'exec',
|
|
33
|
+
'-C',
|
|
34
|
+
projectDir,
|
|
35
|
+
'-s',
|
|
36
|
+
'workspace-write',
|
|
37
|
+
// Keep the workspace-write FILE sandbox, but allow network — the agent's CLI
|
|
38
|
+
// ops (node bin/emberflow.mjs run/get-workflow/…) must reach the runner on
|
|
39
|
+
// 127.0.0.1; workspace-write blocks all network by default (curl → 000).
|
|
40
|
+
'-c',
|
|
41
|
+
'sandbox_workspace_write.network_access=true',
|
|
42
|
+
'-c',
|
|
43
|
+
`model_reasoning_effort=${opts.reasoning ?? 'low'}`,
|
|
44
|
+
'--json',
|
|
45
|
+
...(opts.model ? ['-m', opts.model] : []),
|
|
46
|
+
prompt,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// detached: give codex its own process group so cancel() can tree-kill any
|
|
50
|
+
// subprocesses it spawns (workspace-write), not just the top-level process.
|
|
51
|
+
// stdio ['ignore','pipe','pipe'] → stdin is null, stdout/stderr are Readable.
|
|
52
|
+
const child: ChildProcessByStdio<null, Readable, Readable> = bin.endsWith('.mjs')
|
|
53
|
+
? spawn(process.execPath, [bin, ...args], { stdio: ['ignore', 'pipe', 'pipe'], detached: true })
|
|
54
|
+
: spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'], detached: true });
|
|
55
|
+
|
|
56
|
+
// Buffer only the last ~2KB of stderr — non-fatal in general (e.g. the user's
|
|
57
|
+
// global MCP servers spew auth errors there), but useful as a diagnostic tail
|
|
58
|
+
// when the process dies without a terminal event (see maybeFinalize below).
|
|
59
|
+
let stderrTail = '';
|
|
60
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
61
|
+
stderrTail = (stderrTail + chunk.toString('utf8')).slice(-STDERR_TAIL_BYTES);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const queue: AgentEvent[] = [];
|
|
65
|
+
const resolvers: Array<() => void> = [];
|
|
66
|
+
let done = false;
|
|
67
|
+
let sawTerminal = false;
|
|
68
|
+
|
|
69
|
+
function push(event: AgentEvent) {
|
|
70
|
+
queue.push(event);
|
|
71
|
+
const resolve = resolvers.shift();
|
|
72
|
+
if (resolve) resolve();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function finish() {
|
|
76
|
+
done = true;
|
|
77
|
+
while (resolvers.length) {
|
|
78
|
+
const resolve = resolvers.shift();
|
|
79
|
+
if (resolve) resolve();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
84
|
+
rl.on('line', (line) => {
|
|
85
|
+
const event = parseCodexLine(line);
|
|
86
|
+
if (event) {
|
|
87
|
+
if (event.type === 'done' || event.type === 'error') sawTerminal = true;
|
|
88
|
+
push(event);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Finalize only once BOTH the stdout stream is fully drained (readline 'close')
|
|
93
|
+
// AND the process has exited ('close', which fires after all stdio is flushed —
|
|
94
|
+
// unlike 'exit', which can fire while trailing stdout lines are still buffered).
|
|
95
|
+
let rlClosed = false;
|
|
96
|
+
let procClosed = false;
|
|
97
|
+
let exitCode: number | null = null;
|
|
98
|
+
let escalateTimer: NodeJS.Timeout | undefined;
|
|
99
|
+
|
|
100
|
+
function clearEscalation() {
|
|
101
|
+
if (escalateTimer) {
|
|
102
|
+
clearTimeout(escalateTimer);
|
|
103
|
+
escalateTimer = undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function maybeFinalize() {
|
|
108
|
+
if (!rlClosed || !procClosed) return;
|
|
109
|
+
clearEscalation();
|
|
110
|
+
if (!sawTerminal) {
|
|
111
|
+
if (exitCode === 0) {
|
|
112
|
+
// Clean exit but the model never emitted turn.completed — synthesize a done.
|
|
113
|
+
push({ type: 'done' });
|
|
114
|
+
} else {
|
|
115
|
+
const hint = modelRejectionHint('codex', stderrTail);
|
|
116
|
+
const text = hint ? `codex exited with code ${exitCode} (${hint})` : `codex exited with code ${exitCode}`;
|
|
117
|
+
push({ type: 'error', text });
|
|
118
|
+
}
|
|
119
|
+
sawTerminal = true;
|
|
120
|
+
}
|
|
121
|
+
finish();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
rl.on('close', () => {
|
|
125
|
+
rlClosed = true;
|
|
126
|
+
maybeFinalize();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
child.on('close', (code) => {
|
|
130
|
+
procClosed = true;
|
|
131
|
+
exitCode = code;
|
|
132
|
+
maybeFinalize();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// A failed spawn (missing binary, bad EMBERFLOW_*_BIN, permissions, ...) emits
|
|
136
|
+
// an 'error' event on the child instead of ever exiting/closing normally. Left
|
|
137
|
+
// unhandled, Node rethrows it and crashes the whole runner process — so we
|
|
138
|
+
// must handle it and finalize the stream ourselves. 'close' still fires after
|
|
139
|
+
// a spawn error (with code null), so guard against double-finalizing there.
|
|
140
|
+
let spawnFailed = false;
|
|
141
|
+
child.on('error', (err) => {
|
|
142
|
+
if (spawnFailed) return;
|
|
143
|
+
spawnFailed = true;
|
|
144
|
+
clearEscalation();
|
|
145
|
+
push({ type: 'error', text: `failed to start codex: ${err.message}` });
|
|
146
|
+
sawTerminal = true;
|
|
147
|
+
rlClosed = true;
|
|
148
|
+
procClosed = true;
|
|
149
|
+
finish();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
async function* generate(): AsyncGenerator<AgentEvent> {
|
|
153
|
+
while (true) {
|
|
154
|
+
if (queue.length > 0) {
|
|
155
|
+
yield queue.shift()!;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (done) return;
|
|
159
|
+
await new Promise<void>((resolve) => resolvers.push(resolve));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Kill the whole process group (negative pid) so codex's own subprocesses die
|
|
164
|
+
// too; fall back to a plain kill if the group signal fails (e.g. already gone
|
|
165
|
+
// or no group). Used by both the initial SIGTERM and the SIGKILL escalation.
|
|
166
|
+
function killGroup(signal: NodeJS.Signals) {
|
|
167
|
+
try {
|
|
168
|
+
if (child.pid != null) {
|
|
169
|
+
process.kill(-child.pid, signal);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// group kill failed — fall through to a direct kill
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
child.kill(signal);
|
|
177
|
+
} catch {
|
|
178
|
+
// process already gone — nothing to do
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
events: { [Symbol.asyncIterator]: generate },
|
|
184
|
+
cancel() {
|
|
185
|
+
killGroup('SIGTERM');
|
|
186
|
+
|
|
187
|
+
// If the process (or a stubborn grandchild/MCP server) ignores SIGTERM,
|
|
188
|
+
// escalate to SIGKILL after a grace period so a hung agent can't
|
|
189
|
+
// permanently lock the run slot. Cleared once close/error finalizes.
|
|
190
|
+
clearEscalation();
|
|
191
|
+
escalateTimer = setTimeout(() => {
|
|
192
|
+
escalateTimer = undefined;
|
|
193
|
+
killGroup('SIGKILL');
|
|
194
|
+
}, opts.killGraceMs ?? 3000);
|
|
195
|
+
escalateTimer.unref?.();
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseCodexLine } from './codexParse';
|
|
3
|
+
|
|
4
|
+
describe('parseCodexLine', () => {
|
|
5
|
+
it('maps a command_execution start', () => {
|
|
6
|
+
const e = parseCodexLine('{"type":"item.started","item":{"id":"i3","type":"command_execution","command":"ls","status":"in_progress"}}');
|
|
7
|
+
expect(e).toEqual({ type: 'command', command: 'ls', commandStatus: 'in_progress' });
|
|
8
|
+
});
|
|
9
|
+
it('maps a completed (failed) command with exit code + output', () => {
|
|
10
|
+
const e = parseCodexLine('{"type":"item.completed","item":{"id":"i3","type":"command_execution","command":"curl x","exit_code":6,"aggregated_output":"000","status":"failed"}}');
|
|
11
|
+
expect(e).toEqual({ type: 'command', command: 'curl x', exitCode: 6, output: '000', commandStatus: 'failed' });
|
|
12
|
+
});
|
|
13
|
+
it('maps an agent_message to message', () => {
|
|
14
|
+
expect(parseCodexLine('{"type":"item.completed","item":{"type":"agent_message","text":"Updated file"}}'))
|
|
15
|
+
.toEqual({ type: 'message', text: 'Updated file' });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('maps a non-fatal error ITEM to a warning message (NOT a terminal error)', () => {
|
|
19
|
+
// codex `error` items (e.g. an MCP server failing to auth) must not fail the
|
|
20
|
+
// whole run — only a non-zero process exit does. See codexParse.ts.
|
|
21
|
+
expect(parseCodexLine('{"type":"item.completed","item":{"type":"error","text":"mcp auth failed"}}'))
|
|
22
|
+
.toEqual({ type: 'message', text: '⚠ mcp auth failed' });
|
|
23
|
+
});
|
|
24
|
+
it('reads the error detail from `message` when `text` is absent — the shape current codex ships', () => {
|
|
25
|
+
// Without this, a fatal-cause diagnostic (e.g. "model requires a newer
|
|
26
|
+
// version of Codex") renders as a bare "⚠ error" and the user learns nothing.
|
|
27
|
+
expect(
|
|
28
|
+
parseCodexLine('{"type":"item.completed","item":{"type":"error","message":"model requires a newer version of Codex"}}'),
|
|
29
|
+
).toEqual({ type: 'message', text: '⚠ model requires a newer version of Codex' });
|
|
30
|
+
});
|
|
31
|
+
it('maps an mcp_tool_call (flat server/tool) to an mcp event with status', () => {
|
|
32
|
+
expect(
|
|
33
|
+
parseCodexLine('{"type":"item.started","item":{"type":"mcp_tool_call","server":"emberflow","tool":"run_operation","status":"in_progress"}}'),
|
|
34
|
+
).toEqual({ type: 'mcp', mcpServer: 'emberflow', mcpTool: 'run_operation', mcpStatus: 'in_progress' });
|
|
35
|
+
});
|
|
36
|
+
it('maps a completed mcp_tool_call, reading server/tool from invocation + result', () => {
|
|
37
|
+
expect(
|
|
38
|
+
parseCodexLine('{"type":"item.completed","item":{"type":"mcp_tool_call","invocation":{"server":"emberflow","tool":"run_operation"},"status":"completed","result":{"status":200}}}'),
|
|
39
|
+
).toEqual({ type: 'mcp', mcpServer: 'emberflow', mcpTool: 'run_operation', mcpStatus: 'completed', output: '{\n "status": 200\n}' });
|
|
40
|
+
});
|
|
41
|
+
it('tolerates missing fields on an mcp_tool_call (falls back to mcp/tool)', () => {
|
|
42
|
+
expect(parseCodexLine('{"type":"item.completed","item":{"type":"mcp_tool_call"}}'))
|
|
43
|
+
.toEqual({ type: 'mcp', mcpServer: 'mcp', mcpTool: 'tool', mcpStatus: undefined });
|
|
44
|
+
});
|
|
45
|
+
it('maps turn.completed to done with usage', () => {
|
|
46
|
+
expect(parseCodexLine('{"type":"turn.completed","usage":{"output_tokens":5}}'))
|
|
47
|
+
.toEqual({ type: 'done', usage: { output_tokens: 5 } });
|
|
48
|
+
});
|
|
49
|
+
it('maps thread.started/turn.started to started', () => {
|
|
50
|
+
expect(parseCodexLine('{"type":"thread.started"}')).toEqual({ type: 'started' });
|
|
51
|
+
expect(parseCodexLine('{"type":"turn.started"}')).toEqual({ type: 'started' });
|
|
52
|
+
});
|
|
53
|
+
it('ignores unknown and blank lines', () => {
|
|
54
|
+
expect(parseCodexLine('{"type":"whatever"}')).toBeNull();
|
|
55
|
+
expect(parseCodexLine('')).toBeNull();
|
|
56
|
+
expect(parseCodexLine('not json')).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|