@loicngr/kobo 1.6.14 → 1.7.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/README.md +4 -3
- package/dist/mcp-server/kobo-tasks-server.js +51 -0
- package/dist/server/db/migrations.js +40 -0
- package/dist/server/db/schema.js +7 -5
- package/dist/server/index.js +12 -11
- package/dist/server/routes/workspaces.js +207 -26
- package/dist/server/services/agent/engines/claude-code/capabilities.js +1 -1
- package/dist/server/services/agent/engines/claude-code/engine.js +237 -132
- package/dist/server/services/agent/engines/claude-code/event-mapper.js +234 -0
- package/dist/server/services/agent/engines/claude-code/options-builder.js +68 -0
- package/dist/server/services/agent/engines/claude-code/precompact-hook.js +27 -0
- package/dist/server/services/agent/engines/types.js +1 -0
- package/dist/server/services/agent/orchestrator.js +536 -94
- package/dist/server/services/agent/session-controller.js +14 -43
- package/dist/server/services/auto-loop-service.js +17 -6
- package/dist/server/services/content-migration-service.js +24 -94
- package/dist/server/services/usage/poller.js +4 -1
- package/dist/server/services/wakeup-service.js +8 -9
- package/dist/server/services/workspace-service.js +40 -36
- package/dist/server/utils/git-ops.js +67 -5
- package/package.json +2 -1
- package/src/client/dist/spa/assets/ActivityFeed-CIJPN8TH.js +7 -0
- package/src/client/dist/spa/assets/ActivityFeed-LXnbg3ff.css +1 -0
- package/src/client/dist/spa/assets/ClosePopup-DzB3mDtj.js +1 -0
- package/src/client/dist/spa/assets/CreatePage-BE3xfQsC.css +1 -0
- package/src/client/dist/spa/assets/CreatePage-U6TtJzNe.js +2 -0
- package/src/client/dist/spa/assets/DiffViewer-D1Sdu307.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-Di85TBIi.js +7 -0
- package/src/client/dist/spa/assets/HealthPage-B7aWFxAZ.js +1 -0
- package/src/client/dist/spa/assets/{MainLayout-C3TUaYvQ.js → MainLayout-BHBrz4c9.js} +17 -17
- package/src/client/dist/spa/assets/MainLayout-Dba6SdpU.css +1 -0
- package/src/client/dist/spa/assets/QCheckbox-CcY7ZSk9.js +1 -0
- package/src/client/dist/spa/assets/QChip-BhT0W2Dg.js +1 -0
- package/src/client/dist/spa/assets/QExpansionItem-VS4b4eY6.js +1 -0
- package/src/client/dist/spa/assets/QInput-D4WJro4e.js +1 -0
- package/src/client/dist/spa/assets/QMenu-CchbRXbp.js +1 -0
- package/src/client/dist/spa/assets/{QPage-yqdKDG7-.js → QPage-Cu7zkfc6.js} +1 -1
- package/src/client/dist/spa/assets/QRadio-DaZhdLCg.js +1 -0
- package/src/client/dist/spa/assets/QResizeObserver-Cf79V-VZ.js +1 -0
- package/src/client/dist/spa/assets/QScrollArea-DrVTDLU0.js +1 -0
- package/src/client/dist/spa/assets/QTabPanels-HXz-evuj.js +1 -0
- package/src/client/dist/spa/assets/QToggle-CGpiJLDJ.js +1 -0
- package/src/client/dist/spa/assets/QTooltip-DjJYMTkN.js +1 -0
- package/src/client/dist/spa/assets/SearchPage-CVm-sqxH.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-ayDKGo9H.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-wTBCvK6t.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-DQxGe62K.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-xaVy8s5i.js +4 -0
- package/src/client/dist/spa/assets/build-path-tree-CdY1A6aP.js +1 -0
- package/src/client/dist/spa/assets/{cssMode-wNaxOrgG.js → cssMode-BVNBMOxh.js} +1 -1
- package/src/client/dist/spa/assets/documents-D6A3wRry.js +1 -0
- package/src/client/dist/spa/assets/{editor.api-CcDntllS.js → editor.api-D6Vfp5yv.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-Chu4hc0J.js → editor.main-CTCYF6V4.js} +3 -3
- package/src/client/dist/spa/assets/{expand-template-CcQus77v.js → expand-template-vHV2iwXf.js} +1 -1
- package/src/client/dist/spa/assets/{formatters-CX2gvLFv.js → formatters-ejxELb0M.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-CO_b202E.js → freemarker2-nmzwPmzi.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-CJnTWNLs.js → handlebars-CI9lR7Ef.js} +1 -1
- package/src/client/dist/spa/assets/{html-DeArYseI.js → html-BQ21REnv.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-BnNgEgdx.js → htmlMode-io5J5Qr1.js} +1 -1
- package/src/client/dist/spa/assets/i18n-Do8Kn8n0.js +1 -0
- package/src/client/dist/spa/assets/index-C_e7KOYh.js +2 -0
- package/src/client/dist/spa/assets/{javascript-C0pxfNu4.js → javascript--u9PDBCv.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-ety87201.js → jsonMode-DBG5llk4.js} +1 -1
- package/src/client/dist/spa/assets/{kobo-commands-Cpl4IFon.js → kobo-commands-DiUm1Y34.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-kanevKvC.js → liquid-DxAS4nYF.js} +1 -1
- package/src/client/dist/spa/assets/marked.esm-DuOsJx63.js +60 -0
- package/src/client/dist/spa/assets/{mdx-DkmtbRD7.js → mdx-BNXTiODW.js} +1 -1
- package/src/client/dist/spa/assets/models-DNYEhFF7.js +1 -0
- package/src/client/dist/spa/assets/{monaco.contribution-DsZsua59.js → monaco.contribution-CT3LAK0J.js} +2 -2
- package/src/client/dist/spa/assets/{python-DrxH1xl7.js → python-DztNww13.js} +1 -1
- package/src/client/dist/spa/assets/{razor-CU4khv8N.js → razor-Cyr82NZF.js} +1 -1
- package/src/client/dist/spa/assets/settings-Dbx1_ksA.js +1 -0
- package/src/client/dist/spa/assets/symbols-BVRrMH2r.js +1 -0
- package/src/client/dist/spa/assets/touch-Co9pfjUU.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-CQ5yxoz_.js → tsMode-CbQVgsIP.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-CSwKmP7l.js → typescript-UHOe4d1S.js} +1 -1
- package/src/client/dist/spa/assets/use-checkbox-DzHmcu7s.js +1 -0
- package/src/client/dist/spa/assets/use-id-CDuXkR0Z.js +1 -0
- package/src/client/dist/spa/assets/use-panel-Br8QNRMk.js +1 -0
- package/src/client/dist/spa/assets/{xml-9bnWANPJ.js → xml-DC88eFpV.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-sUtDJGxo.js → yaml-DSTsIRJr.js} +1 -1
- package/src/client/dist/spa/index.html +11 -10
- package/src/mcp-server/kobo-tasks-server.ts +60 -1
- package/dist/server/services/agent/engines/claude-code/args-builder.js +0 -57
- package/dist/server/services/agent/engines/claude-code/mcp-config.js +0 -23
- package/dist/server/services/agent/engines/claude-code/stream-parser.js +0 -386
- package/src/client/dist/spa/assets/ActivityFeed-Be0QQryJ.css +0 -1
- package/src/client/dist/spa/assets/ActivityFeed-BtIOkIy6.js +0 -8
- package/src/client/dist/spa/assets/ClosePopup-DkLittac.js +0 -1
- package/src/client/dist/spa/assets/CreatePage-D6Q3nxkX.js +0 -2
- package/src/client/dist/spa/assets/CreatePage-DJbZH8wp.css +0 -1
- package/src/client/dist/spa/assets/DiffViewer-1s165rFm.css +0 -1
- package/src/client/dist/spa/assets/DiffViewer-D5u9p7il.js +0 -2
- package/src/client/dist/spa/assets/HealthPage-Cr7aAUy6.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-CBnSwSfy.css +0 -1
- package/src/client/dist/spa/assets/QChip-bl3YRhax.js +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-CWw6ZujM.js +0 -1
- package/src/client/dist/spa/assets/QScrollArea-DpCqRRE0.js +0 -1
- package/src/client/dist/spa/assets/QSeparator-DNSiXYrN.js +0 -1
- package/src/client/dist/spa/assets/QSlideTransition-BQxI8l5r.js +0 -1
- package/src/client/dist/spa/assets/QTabPanels-C4bZGqml.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-BIDjo2hJ.js +0 -1
- package/src/client/dist/spa/assets/SearchPage-CavRaij6.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-B8DhSZw7.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-C13T1l_t.js +0 -1
- package/src/client/dist/spa/assets/TouchPan-vsl78kxF.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-BEqEuPrb.js +0 -4
- package/src/client/dist/spa/assets/WorkspacePage-k2pgeRoy.css +0 -1
- package/src/client/dist/spa/assets/build-path-tree-BeAS10oa.js +0 -1
- package/src/client/dist/spa/assets/documents-Cw05r3zs.js +0 -60
- package/src/client/dist/spa/assets/i18n-CuT4b7ns.js +0 -1
- package/src/client/dist/spa/assets/index-CZA4BFN5.js +0 -2
- package/src/client/dist/spa/assets/models-CPFeBEQS.js +0 -1
- package/src/client/dist/spa/assets/private.use-form-Dlb0iQZh.js +0 -1
- package/src/client/dist/spa/assets/scroll-CYWyxBdv.js +0 -1
- package/src/client/dist/spa/assets/settings-CAILUJXO.js +0 -1
- package/src/client/dist/spa/assets/stats-C3n1k51k.js +0 -1
- package/src/client/dist/spa/assets/symbols-DCYodwb2.js +0 -1
- package/src/client/dist/spa/assets/touch-Bj_Fr4kC.js +0 -1
- package/src/client/dist/spa/assets/use-checkbox-B_o-iLG2.js +0 -1
- package/src/client/dist/spa/assets/use-id-C93QQwrt.js +0 -1
- /package/src/client/dist/spa/assets/{QBadge-DqtcDv8D.js → QBadge-fsQ2AokU.js} +0 -0
- /package/src/client/dist/spa/assets/{QItemLabel-Codqjisk.js → QItemLabel-DWwenW2S.js} +0 -0
- /package/src/client/dist/spa/assets/{QItemSection-CiY_LK5Y.js → QItemSection-KFAnxzMK.js} +0 -0
- /package/src/client/dist/spa/assets/{QList-Bl9824vi.js → QList-NmIE6Rd9.js} +0 -0
- /package/src/client/dist/spa/assets/{QSpace-BNr0AftG.js → QSpace-COlmM_4F.js} +0 -0
- /package/src/client/dist/spa/assets/{QSpinnerDots-DEiRooBD.js → QSpinnerDots-DwtnRN2r.js} +0 -0
- /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-r4mAJOHR.js → _plugin-vue_export-helper-B8bB5DBd.js} +0 -0
- /package/src/client/dist/spa/assets/{abap-Bgec7Keq.js → abap-DzK-OTGh.js} +0 -0
- /package/src/client/dist/spa/assets/{apex-VBlPwEoQ.js → apex-Bj60_dRt.js} +0 -0
- /package/src/client/dist/spa/assets/{azcli-DKqrEFBx.js → azcli-B6NwaBAZ.js} +0 -0
- /package/src/client/dist/spa/assets/{bat-DdgQWy_0.js → bat-bf7wXV68.js} +0 -0
- /package/src/client/dist/spa/assets/{bicep-CRMM43EB.js → bicep-C_bg8UgA.js} +0 -0
- /package/src/client/dist/spa/assets/{cameligo-UatALtML.js → cameligo-CTWw4D4B.js} +0 -0
- /package/src/client/dist/spa/assets/{clojure-D8JU08RA.js → clojure-CgdPoH0r.js} +0 -0
- /package/src/client/dist/spa/assets/{coffee-C56wu358.js → coffee-gHQfdA5M.js} +0 -0
- /package/src/client/dist/spa/assets/{cpp-CyZLvhJG.js → cpp-BM4Jj4aW.js} +0 -0
- /package/src/client/dist/spa/assets/{csharp-BJl3ixva.js → csharp-D8-bh4Cd.js} +0 -0
- /package/src/client/dist/spa/assets/{csp-CxEKxmO-.js → csp-CXBxRx0n.js} +0 -0
- /package/src/client/dist/spa/assets/{css-B0t_muXd.js → css-DKjIxrmY.js} +0 -0
- /package/src/client/dist/spa/assets/{cypher-D1hqiMFD.js → cypher-C5e5inIh.js} +0 -0
- /package/src/client/dist/spa/assets/{dart-Bz550Pyv.js → dart-BhRHHm4x.js} +0 -0
- /package/src/client/dist/spa/assets/{dockerfile-CIXgVAuA.js → dockerfile-DW5REF8E.js} +0 -0
- /package/src/client/dist/spa/assets/{ecl-D9qbvZoA.js → ecl-Bw4Hg3n_.js} +0 -0
- /package/src/client/dist/spa/assets/{elixir-b2M38fAy.js → elixir-DHmoBvpZ.js} +0 -0
- /package/src/client/dist/spa/assets/{flow9-Dq1UYMkt.js → flow9-BsFExz3v.js} +0 -0
- /package/src/client/dist/spa/assets/{fsharp-CFNadkg7.js → fsharp-BaeLhgfq.js} +0 -0
- /package/src/client/dist/spa/assets/{go-dSur1iB2.js → go-Bd-NFKIC.js} +0 -0
- /package/src/client/dist/spa/assets/{graphql-qyhAo11d.js → graphql-DZVerJfy.js} +0 -0
- /package/src/client/dist/spa/assets/{hcl-DFzjMyzm.js → hcl-CAVzrZfH.js} +0 -0
- /package/src/client/dist/spa/assets/{ini-TdzA8TIl.js → ini-CyXdX58t.js} +0 -0
- /package/src/client/dist/spa/assets/{is-DUKatk8N.js → is-BbsvEMaT.js} +0 -0
- /package/src/client/dist/spa/assets/{java-CSGA9pkE.js → java-B5pNgvhy.js} +0 -0
- /package/src/client/dist/spa/assets/{julia-9izz5OsY.js → julia-XRhmV3AN.js} +0 -0
- /package/src/client/dist/spa/assets/{kotlin-DuPK7AtF.js → kotlin-DOd3J5vr.js} +0 -0
- /package/src/client/dist/spa/assets/{less-B8d93iCg.js → less-veZSnyw6.js} +0 -0
- /package/src/client/dist/spa/assets/{lexon-DWtEIyu7.js → lexon-QWGkuK0H.js} +0 -0
- /package/src/client/dist/spa/assets/{lua-Ciq0OGgt.js → lua-CYGpjuO5.js} +0 -0
- /package/src/client/dist/spa/assets/{m3-Cki6JWj_.js → m3-yNnrZkdc.js} +0 -0
- /package/src/client/dist/spa/assets/{markdown-Cu47xwU0.js → markdown-BCSWEPSX.js} +0 -0
- /package/src/client/dist/spa/assets/{mips-BM8ui995.js → mips-OpYmcC30.js} +0 -0
- /package/src/client/dist/spa/assets/{msdax-DqLio0_c.js → msdax-2oxoTO9Z.js} +0 -0
- /package/src/client/dist/spa/assets/{mysql-v1wbjJOq.js → mysql-5KlC-K_9.js} +0 -0
- /package/src/client/dist/spa/assets/{objective-c-CQl3PGSB.js → objective-c-CcDCgtLx.js} +0 -0
- /package/src/client/dist/spa/assets/{pascal-D4iW0ZtD.js → pascal-BZGsbaEV.js} +0 -0
- /package/src/client/dist/spa/assets/{pascaligo-BdC9CZdj.js → pascaligo-DtD5qU3G.js} +0 -0
- /package/src/client/dist/spa/assets/{perl-BL10m4XD.js → perl-C1jNNS3E.js} +0 -0
- /package/src/client/dist/spa/assets/{pgsql-Be_oqVo3.js → pgsql-CT0fhiZa.js} +0 -0
- /package/src/client/dist/spa/assets/{php-BtvXSFRI.js → php-D6DrXoPM.js} +0 -0
- /package/src/client/dist/spa/assets/{pla-B2vUy15C.js → pla-b3-HN2pF.js} +0 -0
- /package/src/client/dist/spa/assets/{postiats-CbmTTfXr.js → postiats-Bin2ApVS.js} +0 -0
- /package/src/client/dist/spa/assets/{powerquery-DszLhJGx.js → powerquery-7ASnn-ZG.js} +0 -0
- /package/src/client/dist/spa/assets/{powershell-B0dYktF6.js → powershell-t4p7sU1H.js} +0 -0
- /package/src/client/dist/spa/assets/{protobuf-CZvaj1VX.js → protobuf-BUGeWa_j.js} +0 -0
- /package/src/client/dist/spa/assets/{pug-CPDx1B3S.js → pug-BuKcgC9s.js} +0 -0
- /package/src/client/dist/spa/assets/{qsharp-CDP9TFLl.js → qsharp-DSMtI_O7.js} +0 -0
- /package/src/client/dist/spa/assets/{r-8DbbFX2l.js → r-DMlFgn7A.js} +0 -0
- /package/src/client/dist/spa/assets/{redis-DRWj9MtJ.js → redis-cXItkC5u.js} +0 -0
- /package/src/client/dist/spa/assets/{redshift-C6cElE_5.js → redshift-BZVbW7HE.js} +0 -0
- /package/src/client/dist/spa/assets/{restructuredtext-W9pS9n3m.js → restructuredtext-BzjxwS8h.js} +0 -0
- /package/src/client/dist/spa/assets/{ruby-BKnzWnk-.js → ruby-C5nyLV4l.js} +0 -0
- /package/src/client/dist/spa/assets/{rust-YPCclWwe.js → rust-BcmMsHdf.js} +0 -0
- /package/src/client/dist/spa/assets/{sb-BgM4DTFb.js → sb-Dnb1iy6B.js} +0 -0
- /package/src/client/dist/spa/assets/{scala-fz1OPLMl.js → scala-anMIFYpA.js} +0 -0
- /package/src/client/dist/spa/assets/{scheme-8Uz1RIbu.js → scheme-BItQTe08.js} +0 -0
- /package/src/client/dist/spa/assets/{scss-Djo3IYXr.js → scss-BOv51BJ5.js} +0 -0
- /package/src/client/dist/spa/assets/{shell-CINF5Tx_.js → shell-BsRYRTNN.js} +0 -0
- /package/src/client/dist/spa/assets/{solidity-GgiNEuUm.js → solidity-BtuLgGDx.js} +0 -0
- /package/src/client/dist/spa/assets/{sophia-Culj97P9.js → sophia-B0Vkc5MF.js} +0 -0
- /package/src/client/dist/spa/assets/{sparql-C2ZlpxOY.js → sparql-B7lvkZQM.js} +0 -0
- /package/src/client/dist/spa/assets/{sql-BEf5Pg7Y.js → sql-DvP5MpA3.js} +0 -0
- /package/src/client/dist/spa/assets/{st-CT6UUoeH.js → st-GVUeyB3U.js} +0 -0
- /package/src/client/dist/spa/assets/{swift-B5g0xTG3.js → swift-DSPIoCjm.js} +0 -0
- /package/src/client/dist/spa/assets/{systemverilog-CEgQz9DR.js → systemverilog-Icj2-k23.js} +0 -0
- /package/src/client/dist/spa/assets/{tcl-D0qL2L0I.js → tcl-Cd8KQcm-.js} +0 -0
- /package/src/client/dist/spa/assets/{twig-BFUAVf1E.js → twig-CBHmt8z3.js} +0 -0
- /package/src/client/dist/spa/assets/{typespec-CjVVcNKm.js → typespec-Ckc037mq.js} +0 -0
- /package/src/client/dist/spa/assets/{vb-CZJr-DQz.js → vb-B97GW9Wb.js} +0 -0
- /package/src/client/dist/spa/assets/{vue-i18n-BJlZEYnA.js → vue-i18n-eUDnMrPl.js} +0 -0
- /package/src/client/dist/spa/assets/{wgsl-ivoXUo2e.js → wgsl-DIKmb3YH.js} +0 -0
|
@@ -1,166 +1,271 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { buildClaudeArgs } from './args-builder.js';
|
|
1
|
+
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
4
3
|
import { CLAUDE_CODE_CAPABILITIES } from './capabilities.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { createMapperState, mapSdkMessage } from './event-mapper.js';
|
|
5
|
+
import { buildClaudeOptions } from './options-builder.js';
|
|
6
|
+
import { buildPreCompactCustomInstructions } from './precompact-hook.js';
|
|
7
|
+
function toMcpServersMap(specs) {
|
|
8
|
+
if (!specs || specs.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const map = {};
|
|
11
|
+
for (const s of specs) {
|
|
12
|
+
// `alwaysLoad: true` is required: without it, MCP tools sit behind the
|
|
13
|
+
// SDK's ToolSearch indirection that — even under bypassPermissions —
|
|
14
|
+
// surfaces a "haven't granted it yet" gate. With it, MCP tools behave
|
|
15
|
+
// like built-ins, matching pre-SDK CLI behaviour.
|
|
16
|
+
map[s.name] = { type: 'stdio', command: s.command, args: s.args, env: s.env, alwaysLoad: true };
|
|
17
|
+
}
|
|
18
|
+
return map;
|
|
19
|
+
}
|
|
7
20
|
export function createClaudeCodeEngine() {
|
|
8
21
|
return {
|
|
9
22
|
id: 'claude-code',
|
|
10
23
|
displayName: 'Claude Code',
|
|
11
24
|
capabilities: CLAUDE_CODE_CAPABILITIES,
|
|
12
25
|
async start(options, onEvent) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
26
|
+
const abortController = new AbortController();
|
|
27
|
+
const mapperState = createMapperState();
|
|
28
|
+
// Pending canUseTool callbacks, keyed by SDK ctx.toolUseID.
|
|
29
|
+
const pendingResolvers = new Map();
|
|
30
|
+
const isInteractive = options.agentPermissionMode === 'interactive';
|
|
31
|
+
const canUseTool = (toolName, input, ctx) => {
|
|
32
|
+
const toolCallId = typeof ctx.toolUseID === 'string' && ctx.toolUseID.length > 0 ? ctx.toolUseID : `tu_${nanoid()}`;
|
|
33
|
+
// Non-interactive modes: the SDK has already applied its permissionMode
|
|
34
|
+
// rules before reaching us, so allow through unchanged. AskUserQuestion
|
|
35
|
+
// is the exception — it always defers to the user.
|
|
36
|
+
if (toolName !== 'AskUserQuestion' && !isInteractive) {
|
|
37
|
+
return Promise.resolve({ behavior: 'allow', updatedInput: input });
|
|
38
|
+
}
|
|
39
|
+
const requestKind = toolName === 'AskUserQuestion' ? 'question' : 'permission';
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const resolver = { resolve, input, requestKind };
|
|
42
|
+
pendingResolvers.set(toolCallId, resolver);
|
|
43
|
+
const onAbort = () => {
|
|
44
|
+
if (pendingResolvers.get(toolCallId) === resolver) {
|
|
45
|
+
pendingResolvers.delete(toolCallId);
|
|
46
|
+
const abortError = new Error('Pending user input aborted');
|
|
47
|
+
abortError.name = 'AbortError';
|
|
48
|
+
reject(abortError);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
if (ctx.signal.aborted) {
|
|
52
|
+
onAbort();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
56
|
+
onEvent({
|
|
57
|
+
kind: 'session:user-input-requested',
|
|
58
|
+
requestKind,
|
|
59
|
+
toolCallId,
|
|
60
|
+
toolName,
|
|
61
|
+
payload: input,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
// PreCompact's hookSpecificOutput is missing from the SDK type union
|
|
66
|
+
// (SyncHookJSONOutput omits it), but the runtime accepts
|
|
67
|
+
// `additionalContext` as custom compaction instructions. Cast through
|
|
68
|
+
// `unknown` to satisfy the strict union.
|
|
69
|
+
const hooks = {
|
|
70
|
+
PreCompact: [
|
|
71
|
+
{
|
|
72
|
+
hooks: [
|
|
73
|
+
async () => {
|
|
74
|
+
const reminder = buildPreCompactCustomInstructions(options.workspaceId);
|
|
75
|
+
if (!reminder)
|
|
76
|
+
return {};
|
|
77
|
+
return {
|
|
78
|
+
hookSpecificOutput: {
|
|
79
|
+
hookEventName: 'PreCompact',
|
|
80
|
+
additionalContext: reminder,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
const { options: sdkOptions, effectivePrompt } = buildClaudeOptions({
|
|
19
89
|
prompt: options.prompt,
|
|
20
90
|
model: options.model,
|
|
21
91
|
effort: options.effort,
|
|
22
|
-
|
|
23
|
-
skipPermissions: options.settings.dangerouslySkipPermissions ?? true,
|
|
24
|
-
permissionProfile: options.permissionProfile,
|
|
92
|
+
agentPermissionMode: options.agentPermissionMode ?? 'bypass',
|
|
25
93
|
resumeFromEngineSessionId: options.resumeFromEngineSessionId,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
94
|
+
workingDir: options.workingDir,
|
|
95
|
+
mcpServers: toMcpServersMap(options.mcpServers),
|
|
96
|
+
hooks,
|
|
97
|
+
canUseTool,
|
|
98
|
+
stderr: (data) => {
|
|
99
|
+
const lower = data.toLowerCase();
|
|
100
|
+
if (lower.includes('rate limit exceeded') ||
|
|
101
|
+
lower.includes('rate_limit_exceeded') ||
|
|
102
|
+
(lower.includes('429') && lower.includes('rate')) ||
|
|
103
|
+
lower.includes('quota exceeded')) {
|
|
104
|
+
onEvent({ kind: 'error', category: 'quota', message: data });
|
|
105
|
+
}
|
|
106
|
+
else if (lower.includes('no conversation found with session id')) {
|
|
107
|
+
onEvent({ kind: 'error', category: 'resume_failed', message: data });
|
|
108
|
+
}
|
|
109
|
+
else if (data.trim().length > 0) {
|
|
110
|
+
console.warn(`[claude-engine stderr] ${data}`);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
38
113
|
});
|
|
114
|
+
sdkOptions.abortController = abortController;
|
|
115
|
+
const q = query({ prompt: effectivePrompt, options: sdkOptions });
|
|
39
116
|
let discoveredSessionId;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
discoveredSessionId = ev.engineSessionId;
|
|
117
|
+
// A throwing onEvent handler (e.g. DB query against a closed connection
|
|
118
|
+
// during async test teardown) must not escape as an unhandled rejection.
|
|
119
|
+
const safeEmit = (ev) => {
|
|
120
|
+
try {
|
|
45
121
|
onEvent(ev);
|
|
46
122
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// arbitrary byte chunks, and restrict quota detection to clear rate-
|
|
50
|
-
// limit signals (not every occurrence of the word "rate" or "quota").
|
|
51
|
-
// Non-quota stderr lines are logged to the console but do NOT emit
|
|
52
|
-
// an error event — this avoids false positives flooding the UI.
|
|
53
|
-
const stderrRl = proc.stderr
|
|
54
|
-
? readline.createInterface({
|
|
55
|
-
input: proc.stderr,
|
|
56
|
-
crlfDelay: Number.POSITIVE_INFINITY,
|
|
57
|
-
})
|
|
58
|
-
: undefined;
|
|
59
|
-
// Known benign stderr lines from the Claude CLI that should NOT be
|
|
60
|
-
// logged — they flood the dev console and carry no actionable info.
|
|
61
|
-
// Strip ANSI color codes before matching.
|
|
62
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escapes by design
|
|
63
|
-
const stripAnsi = (s) => s.replace(/\u001b\[\d+m/g, '');
|
|
64
|
-
function isBenignStderr(line) {
|
|
65
|
-
const cleaned = stripAnsi(line).trim();
|
|
66
|
-
return /^warning: no stdin data received in \d+s/i.test(cleaned);
|
|
67
|
-
}
|
|
68
|
-
stderrRl?.on('line', (line) => {
|
|
69
|
-
const lower = line.toLowerCase();
|
|
70
|
-
const isQuota = lower.includes('rate limit exceeded') ||
|
|
71
|
-
lower.includes('rate_limit_exceeded') ||
|
|
72
|
-
(lower.includes('429') && lower.includes('rate')) ||
|
|
73
|
-
lower.includes('quota exceeded');
|
|
74
|
-
const isResumeFailed = lower.includes('no conversation found with session id');
|
|
75
|
-
if (isQuota) {
|
|
76
|
-
onEvent({ kind: 'error', category: 'quota', message: line });
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.error('[claude-engine] onEvent handler threw:', err);
|
|
77
125
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
126
|
+
};
|
|
127
|
+
let iteratorRunning = false;
|
|
128
|
+
let userInterrupted = false;
|
|
129
|
+
const iteratorPromise = (async () => {
|
|
130
|
+
iteratorRunning = true;
|
|
131
|
+
try {
|
|
132
|
+
for await (const msg of q) {
|
|
133
|
+
const events = mapSdkMessage(msg, mapperState);
|
|
134
|
+
for (const ev of events) {
|
|
135
|
+
if (ev.kind === 'session:started')
|
|
136
|
+
discoveredSessionId = ev.engineSessionId;
|
|
137
|
+
safeEmit(ev);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// If the SDK ended with a `result.subtype === 'error_*'`, the
|
|
141
|
+
// event-mapper already surfaced an `error` event but the iterator
|
|
142
|
+
// still terminated naturally. Reflect that in the session:ended
|
|
143
|
+
// reason so the orchestrator transitions the workspace to `error`.
|
|
144
|
+
safeEmit({
|
|
145
|
+
kind: 'session:ended',
|
|
146
|
+
reason: mapperState.sawErrorResult ? 'error' : 'completed',
|
|
147
|
+
exitCode: mapperState.sawErrorResult ? null : 0,
|
|
148
|
+
});
|
|
81
149
|
}
|
|
82
|
-
|
|
83
|
-
|
|
150
|
+
catch (err) {
|
|
151
|
+
// Treat any abort we triggered (stop() → abortController.abort()) as
|
|
152
|
+
// a clean kill. The SDK sometimes throws a generic Error with message
|
|
153
|
+
// "Claude Code process aborted by user" instead of a typed AbortError.
|
|
154
|
+
const error = err;
|
|
155
|
+
const isAbort = userInterrupted ||
|
|
156
|
+
error.name === 'AbortError' ||
|
|
157
|
+
abortController.signal.aborted ||
|
|
158
|
+
/aborted by user|process aborted|abortError|ede_diagnostic/i.test(error.message ?? '');
|
|
159
|
+
if (isAbort) {
|
|
160
|
+
safeEmit({ kind: 'session:ended', reason: 'killed', exitCode: null });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
safeEmit({
|
|
164
|
+
kind: 'error',
|
|
165
|
+
category: 'spawn_failed',
|
|
166
|
+
message: error.message,
|
|
167
|
+
});
|
|
168
|
+
safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
|
|
169
|
+
}
|
|
84
170
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
kind: 'session:ended',
|
|
102
|
-
reason: code === 0 ? 'completed' : code === null ? 'killed' : 'error',
|
|
103
|
-
exitCode: code,
|
|
104
|
-
});
|
|
105
|
-
});
|
|
171
|
+
finally {
|
|
172
|
+
// Drain any callback still pending (SDK terminated while awaiting an
|
|
173
|
+
// answer). canUseTool's abort path covers signalled stops; this
|
|
174
|
+
// covers natural iterator completion.
|
|
175
|
+
for (const resolver of pendingResolvers.values()) {
|
|
176
|
+
try {
|
|
177
|
+
resolver.resolve({ behavior: 'deny', message: 'session ended', interrupt: false });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// best-effort
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
pendingResolvers.clear();
|
|
184
|
+
iteratorRunning = false;
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
106
187
|
const engineProcess = {
|
|
107
188
|
get pid() {
|
|
108
|
-
return
|
|
189
|
+
return undefined;
|
|
109
190
|
},
|
|
110
191
|
get engineSessionId() {
|
|
111
192
|
return discoveredSessionId;
|
|
112
193
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
throw new Error('Agent stdin not writable');
|
|
116
|
-
proc.stdin.write(`${text}\n`);
|
|
194
|
+
isAlive() {
|
|
195
|
+
return iteratorRunning;
|
|
117
196
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
process.kill(proc.pid, 'SIGINT');
|
|
197
|
+
sendMessage() {
|
|
198
|
+
throw new Error('sendMessage not supported in single-shot SDK mode');
|
|
121
199
|
},
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let resolved = false;
|
|
127
|
-
let killTimer;
|
|
128
|
-
let hardTimeout;
|
|
129
|
-
const doResolve = () => {
|
|
130
|
-
if (resolved)
|
|
131
|
-
return;
|
|
132
|
-
resolved = true;
|
|
133
|
-
if (killTimer)
|
|
134
|
-
clearTimeout(killTimer);
|
|
135
|
-
if (hardTimeout)
|
|
136
|
-
clearTimeout(hardTimeout);
|
|
137
|
-
resolve();
|
|
138
|
-
};
|
|
139
|
-
proc.once('exit', doResolve);
|
|
200
|
+
interrupt() {
|
|
201
|
+
userInterrupted = true;
|
|
202
|
+
const qq = q;
|
|
203
|
+
if (typeof qq.interrupt === 'function') {
|
|
140
204
|
try {
|
|
141
|
-
|
|
205
|
+
const r = qq.interrupt();
|
|
206
|
+
if (r && typeof r.catch === 'function') {
|
|
207
|
+
;
|
|
208
|
+
r.catch(() => {
|
|
209
|
+
/* ignore */
|
|
210
|
+
});
|
|
211
|
+
}
|
|
142
212
|
}
|
|
143
213
|
catch {
|
|
144
|
-
|
|
214
|
+
abortController.abort();
|
|
145
215
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
abortController.abort();
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
async stop() {
|
|
222
|
+
abortController.abort();
|
|
223
|
+
try {
|
|
224
|
+
await iteratorPromise;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// swallow — best effort
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
resolvePendingUserInput(toolCallId, response) {
|
|
231
|
+
const resolver = pendingResolvers.get(toolCallId);
|
|
232
|
+
if (!resolver)
|
|
233
|
+
return false;
|
|
234
|
+
pendingResolvers.delete(toolCallId);
|
|
235
|
+
if (response.kind === 'question') {
|
|
236
|
+
// Echo the original questions array + answers so the SDK
|
|
237
|
+
// reconstructs the AskUserQuestion tool input.
|
|
238
|
+
const original = resolver.input;
|
|
239
|
+
const questions = original.questions;
|
|
240
|
+
resolver.resolve({
|
|
241
|
+
behavior: 'allow',
|
|
242
|
+
updatedInput: {
|
|
243
|
+
...(typeof questions !== 'undefined' ? { questions } : {}),
|
|
244
|
+
answers: response.answers,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
if (response.kind === 'question-cancel') {
|
|
250
|
+
// Deny so the agent gets an error tool_result and can adapt.
|
|
251
|
+
resolver.resolve({
|
|
252
|
+
behavior: 'deny',
|
|
253
|
+
message: response.reason ?? 'User cancelled the question',
|
|
254
|
+
interrupt: false,
|
|
255
|
+
});
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (response.kind === 'permission-allow') {
|
|
259
|
+
resolver.resolve({ behavior: 'allow', updatedInput: resolver.input });
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
// permission-deny
|
|
263
|
+
resolver.resolve({
|
|
264
|
+
behavior: 'deny',
|
|
265
|
+
message: response.reason ?? 'denied by user',
|
|
266
|
+
interrupt: false,
|
|
163
267
|
});
|
|
268
|
+
return true;
|
|
164
269
|
},
|
|
165
270
|
};
|
|
166
271
|
return engineProcess;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
2
|
+
// `rate_limit_info` is shaped for claude.ai subscriptions and may evolve.
|
|
3
|
+
// Keep the defensive normalisation so a schema bump doesn't drop bucket info.
|
|
4
|
+
function normalizeResetsAt(raw) {
|
|
5
|
+
if (typeof raw === 'string' && raw.length > 0)
|
|
6
|
+
return raw;
|
|
7
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
8
|
+
return new Date(raw * 1000).toISOString();
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
function extractUsedPct(source) {
|
|
12
|
+
const raw = (source.utilization ?? source.used_percent ?? source.percent_used ?? source.usedPct);
|
|
13
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
14
|
+
return raw <= 1 ? raw * 100 : raw;
|
|
15
|
+
const used = source.used ?? source.current ?? source.spent;
|
|
16
|
+
const limit = source.limit ?? source.max ?? source.allowed;
|
|
17
|
+
if (typeof used === 'number' && typeof limit === 'number' && limit > 0)
|
|
18
|
+
return (used / limit) * 100;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function makeBucket(id, source) {
|
|
22
|
+
const usedPct = extractUsedPct(source) ?? source.__fallbackPct ?? null;
|
|
23
|
+
if (usedPct === null)
|
|
24
|
+
return null;
|
|
25
|
+
const resetsAt = normalizeResetsAt(source.resets_at ?? source.reset_at ?? source.resetsAt ?? source.resetAt);
|
|
26
|
+
const label = (typeof source.label === 'string' && source.label) || undefined;
|
|
27
|
+
const used = source.used ?? source.current ?? source.spent;
|
|
28
|
+
const limit = source.limit ?? source.max ?? source.allowed;
|
|
29
|
+
const details = used !== undefined && limit !== undefined ? `${String(used)} / ${String(limit)}` : undefined;
|
|
30
|
+
return { id, label, usedPct: Math.max(0, Math.min(100, usedPct)), resetsAt, details };
|
|
31
|
+
}
|
|
32
|
+
function normalizeRateLimitInfo(info) {
|
|
33
|
+
const buckets = [];
|
|
34
|
+
if (typeof info.rateLimitType === 'string') {
|
|
35
|
+
const b = makeBucket(info.rateLimitType, { ...info, __fallbackPct: 0 });
|
|
36
|
+
if (b)
|
|
37
|
+
buckets.push(b);
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(info.buckets)) {
|
|
40
|
+
for (const entry of info.buckets) {
|
|
41
|
+
if (!entry || typeof entry !== 'object')
|
|
42
|
+
continue;
|
|
43
|
+
const obj = entry;
|
|
44
|
+
const id = (typeof obj.id === 'string' && obj.id) ||
|
|
45
|
+
(typeof obj.name === 'string' && obj.name) ||
|
|
46
|
+
(typeof obj.rateLimitType === 'string' && obj.rateLimitType) ||
|
|
47
|
+
'unknown';
|
|
48
|
+
const b = makeBucket(id, obj);
|
|
49
|
+
if (b)
|
|
50
|
+
buckets.push(b);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { buckets };
|
|
54
|
+
}
|
|
55
|
+
export function createMapperState() {
|
|
56
|
+
return { sessionStartedEmitted: false, openMessages: new Map(), sawErrorResult: false };
|
|
57
|
+
}
|
|
58
|
+
/** Known SDK `result` subtypes that indicate the run failed. */
|
|
59
|
+
const KNOWN_ERROR_RESULT_SUBTYPES = new Set(['error_max_turns', 'error_during_execution']);
|
|
60
|
+
function isErrorResultSubtype(subtype) {
|
|
61
|
+
if (!subtype)
|
|
62
|
+
return false;
|
|
63
|
+
if (KNOWN_ERROR_RESULT_SUBTYPES.has(subtype))
|
|
64
|
+
return true;
|
|
65
|
+
return subtype.startsWith('error');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Maps a single typed `SDKMessage` to zero or more `AgentEvent`s, mutating
|
|
69
|
+
* `state` as needed.
|
|
70
|
+
*/
|
|
71
|
+
export function mapSdkMessage(msg, state) {
|
|
72
|
+
// Treat as a generic record — the SDK discriminated union is too broad to
|
|
73
|
+
// narrow per branch here.
|
|
74
|
+
const parsed = msg;
|
|
75
|
+
const type = parsed.type;
|
|
76
|
+
const subtype = parsed.subtype;
|
|
77
|
+
const sessionId = typeof parsed.session_id === 'string' ? parsed.session_id : undefined;
|
|
78
|
+
const events = [];
|
|
79
|
+
// Rate-limit events are top-level in the SDK (no longer nested under `system`).
|
|
80
|
+
if (type === 'rate_limit_event') {
|
|
81
|
+
const info = parsed.rate_limit_info;
|
|
82
|
+
if (info && typeof info === 'object') {
|
|
83
|
+
events.push({ kind: 'rate_limit', info: normalizeRateLimitInfo(info) });
|
|
84
|
+
}
|
|
85
|
+
return events;
|
|
86
|
+
}
|
|
87
|
+
if (type === 'system') {
|
|
88
|
+
if (subtype === 'compact' || subtype === 'compact_boundary') {
|
|
89
|
+
events.push({ kind: 'session:compacted' });
|
|
90
|
+
return events;
|
|
91
|
+
}
|
|
92
|
+
if (subtype === 'task_started' || subtype === 'task_progress' || subtype === 'task_notification') {
|
|
93
|
+
const toolCallId = typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : undefined;
|
|
94
|
+
if (toolCallId) {
|
|
95
|
+
const usage = parsed.usage;
|
|
96
|
+
const taskStatus = typeof parsed.status === 'string' ? parsed.status : undefined;
|
|
97
|
+
const isDone = subtype === 'task_notification' &&
|
|
98
|
+
taskStatus !== undefined &&
|
|
99
|
+
['completed', 'stopped', 'failed', 'cancelled'].includes(taskStatus);
|
|
100
|
+
events.push({
|
|
101
|
+
kind: 'subagent:progress',
|
|
102
|
+
toolCallId,
|
|
103
|
+
status: isDone ? 'done' : 'running',
|
|
104
|
+
description: typeof parsed.description === 'string' ? parsed.description : undefined,
|
|
105
|
+
taskType: typeof parsed.task_type === 'string' ? parsed.task_type : undefined,
|
|
106
|
+
lastToolName: typeof parsed.last_tool_name === 'string' ? parsed.last_tool_name : undefined,
|
|
107
|
+
totalTokens: typeof usage?.total_tokens === 'number' ? usage.total_tokens : undefined,
|
|
108
|
+
toolUses: typeof usage?.tool_uses === 'number' ? usage.tool_uses : undefined,
|
|
109
|
+
durationMs: typeof usage?.duration_ms === 'number' ? usage.duration_ms : undefined,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return events;
|
|
113
|
+
}
|
|
114
|
+
if (subtype === 'init') {
|
|
115
|
+
if (sessionId && (!state.sessionStartedEmitted || state.sessionId !== sessionId)) {
|
|
116
|
+
events.push({
|
|
117
|
+
kind: 'session:started',
|
|
118
|
+
engineSessionId: sessionId,
|
|
119
|
+
model: typeof parsed.model === 'string' ? parsed.model : undefined,
|
|
120
|
+
});
|
|
121
|
+
state.sessionStartedEmitted = true;
|
|
122
|
+
state.sessionId = sessionId;
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(parsed.slash_commands) && parsed.slash_commands.length > 0) {
|
|
125
|
+
events.push({ kind: 'skills:discovered', skills: parsed.slash_commands });
|
|
126
|
+
}
|
|
127
|
+
return events;
|
|
128
|
+
}
|
|
129
|
+
return events;
|
|
130
|
+
}
|
|
131
|
+
if (type === 'assistant') {
|
|
132
|
+
const message = parsed.message;
|
|
133
|
+
const messageId = typeof message?.id === 'string' ? message.id : 'unknown';
|
|
134
|
+
const content = Array.isArray(message?.content) ? message?.content : [];
|
|
135
|
+
// SDK runs sometimes finish implicitly when the next turn begins. Close
|
|
136
|
+
// stale openMessages so the UI's streaming spinner doesn't hang.
|
|
137
|
+
for (const openId of Array.from(state.openMessages.keys())) {
|
|
138
|
+
if (openId !== messageId) {
|
|
139
|
+
events.push({ kind: 'message:end', messageId: openId });
|
|
140
|
+
state.openMessages.delete(openId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!state.openMessages.has(messageId)) {
|
|
144
|
+
state.openMessages.set(messageId, { sawText: false });
|
|
145
|
+
}
|
|
146
|
+
const msgState = state.openMessages.get(messageId);
|
|
147
|
+
if (!msgState)
|
|
148
|
+
return events;
|
|
149
|
+
for (const block of content) {
|
|
150
|
+
const blockType = block.type;
|
|
151
|
+
if (blockType === 'text' && typeof block.text === 'string') {
|
|
152
|
+
events.push({ kind: 'message:text', messageId, text: block.text, streaming: true });
|
|
153
|
+
msgState.sawText = true;
|
|
154
|
+
if (block.text.includes('[BRAINSTORM_COMPLETE]')) {
|
|
155
|
+
events.push({ kind: 'session:brainstorm-complete' });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (blockType === 'tool_use') {
|
|
159
|
+
events.push({
|
|
160
|
+
kind: 'tool:call',
|
|
161
|
+
messageId,
|
|
162
|
+
toolCallId: typeof block.id === 'string' ? block.id : 'unknown',
|
|
163
|
+
name: typeof block.name === 'string' ? block.name : 'unknown',
|
|
164
|
+
input: block.input ?? {},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (blockType === 'thinking') {
|
|
168
|
+
events.push({
|
|
169
|
+
kind: 'message:thinking',
|
|
170
|
+
messageId,
|
|
171
|
+
text: typeof block.thinking === 'string' ? block.thinking : '',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Only terminal turns carry non-null `stop_reason`; intermediate deltas
|
|
176
|
+
// have `null` and must NOT trigger message:end.
|
|
177
|
+
const stopReason = message?.stop_reason;
|
|
178
|
+
const isStop = parsed.message_stop === true || (stopReason !== undefined && stopReason !== null);
|
|
179
|
+
if (isStop) {
|
|
180
|
+
events.push({ kind: 'message:end', messageId });
|
|
181
|
+
state.openMessages.delete(messageId);
|
|
182
|
+
}
|
|
183
|
+
return events;
|
|
184
|
+
}
|
|
185
|
+
if (type === 'user') {
|
|
186
|
+
const message = parsed.message;
|
|
187
|
+
const content = Array.isArray(message?.content) ? message?.content : [];
|
|
188
|
+
for (const block of content) {
|
|
189
|
+
if (block.type === 'tool_result') {
|
|
190
|
+
events.push({
|
|
191
|
+
kind: 'tool:result',
|
|
192
|
+
toolCallId: typeof block.tool_use_id === 'string' ? block.tool_use_id : 'unknown',
|
|
193
|
+
output: block.content ?? null,
|
|
194
|
+
isError: block.is_error === true,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return events;
|
|
199
|
+
}
|
|
200
|
+
if (type === 'result') {
|
|
201
|
+
// Terminal event — close any still-streaming messages.
|
|
202
|
+
for (const openId of Array.from(state.openMessages.keys())) {
|
|
203
|
+
events.push({ kind: 'message:end', messageId: openId });
|
|
204
|
+
state.openMessages.delete(openId);
|
|
205
|
+
}
|
|
206
|
+
// Detect error variants of `result` (e.g. `error_max_turns`,
|
|
207
|
+
// `error_during_execution`) and surface them as a proper `error` event so
|
|
208
|
+
// the orchestrator can transition the workspace to `error` instead of
|
|
209
|
+
// `completed`. The flag on `state` lets the engine override the
|
|
210
|
+
// post-loop session:ended reason.
|
|
211
|
+
if (isErrorResultSubtype(subtype)) {
|
|
212
|
+
state.sawErrorResult = true;
|
|
213
|
+
const detail = (typeof parsed.error === 'string' && parsed.error) || (typeof parsed.result === 'string' && parsed.result) || '';
|
|
214
|
+
const message = detail ? `Agent run failed (${subtype}): ${detail}` : `Agent run failed (${subtype})`;
|
|
215
|
+
events.push({ kind: 'error', category: 'other', message });
|
|
216
|
+
}
|
|
217
|
+
const usage = parsed.usage;
|
|
218
|
+
if (usage) {
|
|
219
|
+
const costUsd = typeof parsed.total_cost_usd === 'number' ? parsed.total_cost_usd : undefined;
|
|
220
|
+
events.push({
|
|
221
|
+
kind: 'usage',
|
|
222
|
+
inputTokens: Number(usage.input_tokens ?? 0),
|
|
223
|
+
outputTokens: Number(usage.output_tokens ?? 0),
|
|
224
|
+
cacheRead: typeof usage.cache_read_input_tokens === 'number' ? usage.cache_read_input_tokens : undefined,
|
|
225
|
+
cacheWrite: typeof usage.cache_creation_input_tokens === 'number'
|
|
226
|
+
? usage.cache_creation_input_tokens
|
|
227
|
+
: undefined,
|
|
228
|
+
costUsd,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return events;
|
|
232
|
+
}
|
|
233
|
+
return events;
|
|
234
|
+
}
|