@kolisachint/hoocode-agent 0.4.162 → 0.4.163
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/CHANGELOG.md +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +0 -9
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +0 -40
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -50
- package/dist/config.js.map +1 -1
- package/dist/core/agent-frontmatter.d.ts +0 -28
- package/dist/core/agent-frontmatter.d.ts.map +1 -1
- package/dist/core/agent-frontmatter.js.map +1 -1
- package/dist/core/auth-guidance.d.ts +0 -1
- package/dist/core/auth-guidance.d.ts.map +1 -1
- package/dist/core/auth-guidance.js +1 -1
- package/dist/core/auth-guidance.js.map +1 -1
- package/dist/core/embsearch/chunker.d.ts +0 -2
- package/dist/core/embsearch/chunker.d.ts.map +1 -1
- package/dist/core/embsearch/chunker.js +1 -1
- package/dist/core/embsearch/chunker.js.map +1 -1
- package/dist/core/embsearch/index-meta.d.ts +0 -2
- package/dist/core/embsearch/index-meta.d.ts.map +1 -1
- package/dist/core/embsearch/index-meta.js +0 -6
- package/dist/core/embsearch/index-meta.js.map +1 -1
- package/dist/core/export-html/ansi-to-html.d.ts +0 -4
- package/dist/core/export-html/ansi-to-html.d.ts.map +1 -1
- package/dist/core/export-html/ansi-to-html.js +1 -1
- package/dist/core/export-html/ansi-to-html.js.map +1 -1
- package/dist/core/extensions/index.d.ts +3 -3
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/plugins/authoring.d.ts +2 -2
- package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
- package/dist/core/extensions/plugins/authoring.js +0 -0
- package/dist/core/extensions/plugins/authoring.js.map +1 -1
- package/dist/core/extensions/plugins/formats/index.d.ts +2 -6
- package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/index.js +2 -3
- package/dist/core/extensions/plugins/formats/index.js.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.d.ts +0 -2
- package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.js +1 -1
- package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
- package/dist/core/extensions/plugins/formats/types.d.ts +4 -3
- package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/types.js.map +1 -1
- package/dist/core/extensions/plugins/install.d.ts +0 -2
- package/dist/core/extensions/plugins/install.d.ts.map +1 -1
- package/dist/core/extensions/plugins/install.js +1 -1
- package/dist/core/extensions/plugins/install.js.map +1 -1
- package/dist/core/extensions/plugins/manifest.d.ts +0 -6
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/manifest.js +0 -6
- package/dist/core/extensions/plugins/manifest.js.map +1 -1
- package/dist/core/extensions/plugins/marketplace.d.ts +3 -3
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
- package/dist/core/extensions/plugins/marketplace.js.map +1 -1
- package/dist/core/extensions/types.d.ts +28 -28
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +3 -3
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-resolver.d.ts +0 -7
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +0 -56
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-resource-discovery.d.ts +0 -10
- package/dist/core/package-resource-discovery.d.ts.map +1 -1
- package/dist/core/package-resource-discovery.js +7 -7
- package/dist/core/package-resource-discovery.js.map +1 -1
- package/dist/core/provider-health.d.ts +0 -2
- package/dist/core/provider-health.d.ts.map +1 -1
- package/dist/core/provider-health.js +1 -1
- package/dist/core/provider-health.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts +0 -4
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +0 -15
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -7
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/search/eval.d.ts +0 -3
- package/dist/core/search/eval.d.ts.map +1 -1
- package/dist/core/search/eval.js +1 -1
- package/dist/core/search/eval.js.map +1 -1
- package/dist/core/search/types.d.ts +2 -1
- package/dist/core/search/types.d.ts.map +1 -1
- package/dist/core/search/types.js.map +1 -1
- package/dist/core/session-cwd.d.ts +0 -1
- package/dist/core/session-cwd.d.ts.map +1 -1
- package/dist/core/session-cwd.js +1 -1
- package/dist/core/session-cwd.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +0 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +9 -8
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/subagent-inbox.d.ts +1 -3
- package/dist/core/subagent-inbox.d.ts.map +1 -1
- package/dist/core/subagent-inbox.js +1 -1
- package/dist/core/subagent-inbox.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +0 -13
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +2 -2
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/task-store.d.ts +3 -3
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/team-auto.d.ts +0 -2
- package/dist/core/team-auto.d.ts.map +1 -1
- package/dist/core/team-auto.js +1 -1
- package/dist/core/team-auto.js.map +1 -1
- package/dist/core/token-budget.d.ts +0 -13
- package/dist/core/token-budget.d.ts.map +1 -1
- package/dist/core/token-budget.js.map +1 -1
- package/dist/core/tools/browser/browser-continue.d.ts +1 -2
- package/dist/core/tools/browser/browser-continue.d.ts.map +1 -1
- package/dist/core/tools/browser/browser-continue.js.map +1 -1
- package/dist/core/tools/browser/browser-run.d.ts +1 -2
- package/dist/core/tools/browser/browser-run.d.ts.map +1 -1
- package/dist/core/tools/browser/browser-run.js.map +1 -1
- package/dist/core/tools/browser/browsertools-shared.d.ts +1 -14
- package/dist/core/tools/browser/browsertools-shared.d.ts.map +1 -1
- package/dist/core/tools/browser/browsertools-shared.js +4 -4
- package/dist/core/tools/browser/browsertools-shared.js.map +1 -1
- package/dist/core/tools/browser/index.d.ts +2 -2
- package/dist/core/tools/browser/index.d.ts.map +1 -1
- package/dist/core/tools/browser/index.js +2 -2
- package/dist/core/tools/browser/index.js.map +1 -1
- package/dist/core/tools/doc/docedit.d.ts +1 -2
- package/dist/core/tools/doc/docedit.d.ts.map +1 -1
- package/dist/core/tools/doc/docedit.js.map +1 -1
- package/dist/core/tools/doc/docgrep.d.ts +1 -2
- package/dist/core/tools/doc/docgrep.d.ts.map +1 -1
- package/dist/core/tools/doc/docgrep.js.map +1 -1
- package/dist/core/tools/doc/docpeek.d.ts +1 -2
- package/dist/core/tools/doc/docpeek.d.ts.map +1 -1
- package/dist/core/tools/doc/docpeek.js.map +1 -1
- package/dist/core/tools/doc/docread.d.ts +1 -2
- package/dist/core/tools/doc/docread.d.ts.map +1 -1
- package/dist/core/tools/doc/docread.js.map +1 -1
- package/dist/core/tools/doc/docscan.d.ts +1 -2
- package/dist/core/tools/doc/docscan.d.ts.map +1 -1
- package/dist/core/tools/doc/docscan.js.map +1 -1
- package/dist/core/tools/doc/docwrite.d.ts +1 -2
- package/dist/core/tools/doc/docwrite.d.ts.map +1 -1
- package/dist/core/tools/doc/docwrite.js.map +1 -1
- package/dist/core/tools/doc/index.d.ts +6 -6
- package/dist/core/tools/doc/index.d.ts.map +1 -1
- package/dist/core/tools/doc/index.js +6 -6
- package/dist/core/tools/doc/index.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +0 -38
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +2 -9
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/filetools-shared.d.ts +7 -10
- package/dist/core/tools/filetools-shared.d.ts.map +1 -1
- package/dist/core/tools/filetools-shared.js +2 -2
- package/dist/core/tools/filetools-shared.js.map +1 -1
- package/dist/core/tools/index.d.ts +0 -4
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +0 -7
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/native-search.d.ts +3 -2
- package/dist/core/tools/native-search.d.ts.map +1 -1
- package/dist/core/tools/native-search.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +0 -10
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +2 -2
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/plugins.d.ts +1 -20
- package/dist/core/tools/plugins.d.ts.map +1 -1
- package/dist/core/tools/plugins.js +2 -2
- package/dist/core/tools/plugins.js.map +1 -1
- package/dist/core/tools/propose-plugin.d.ts +0 -12
- package/dist/core/tools/propose-plugin.d.ts.map +1 -1
- package/dist/core/tools/propose-plugin.js +0 -1
- package/dist/core/tools/propose-plugin.js.map +1 -1
- package/dist/core/tools/read-dedup.d.ts +0 -2
- package/dist/core/tools/read-dedup.d.ts.map +1 -1
- package/dist/core/tools/read-dedup.js +1 -1
- package/dist/core/tools/read-dedup.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +0 -5
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/search.d.ts +1 -4
- package/dist/core/tools/search.d.ts.map +1 -1
- package/dist/core/tools/search.js +0 -4
- package/dist/core/tools/search.js.map +1 -1
- package/dist/core/tools/webfetch.d.ts +1 -2
- package/dist/core/tools/webfetch.d.ts.map +1 -1
- package/dist/core/tools/webfetch.js.map +1 -1
- package/dist/core/tools/websearch.d.ts +1 -2
- package/dist/core/tools/websearch.d.ts.map +1 -1
- package/dist/core/tools/websearch.js.map +1 -1
- package/dist/core/tools/webtools-shared.d.ts +3 -5
- package/dist/core/tools/webtools-shared.d.ts.map +1 -1
- package/dist/core/tools/webtools-shared.js +1 -1
- package/dist/core/tools/webtools-shared.js.map +1 -1
- package/dist/core/warm-subagent-pool-instance.d.ts +0 -4
- package/dist/core/warm-subagent-pool-instance.d.ts.map +1 -1
- package/dist/core/warm-subagent-pool-instance.js +0 -10
- package/dist/core/warm-subagent-pool-instance.js.map +1 -1
- package/dist/core/wordmark.d.ts +1 -9
- package/dist/core/wordmark.d.ts.map +1 -1
- package/dist/core/wordmark.js +2 -26
- package/dist/core/wordmark.js.map +1 -1
- package/dist/extensions/core/config.d.ts +4 -3
- package/dist/extensions/core/config.d.ts.map +1 -1
- package/dist/extensions/core/config.js.map +1 -1
- package/dist/extensions/core/mcp-loader.d.ts +0 -18
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/extensions/core/prompt-reactive/policy.d.ts +0 -8
- package/dist/extensions/core/prompt-reactive/policy.d.ts.map +1 -1
- package/dist/extensions/core/prompt-reactive/policy.js +1 -1
- package/dist/extensions/core/prompt-reactive/policy.js.map +1 -1
- package/dist/migrations.d.ts +0 -16
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +2 -2
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/brand.d.ts +0 -4
- package/dist/modes/interactive/brand.d.ts.map +1 -1
- package/dist/modes/interactive/brand.js +0 -4
- package/dist/modes/interactive/brand.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +0 -4
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +0 -5
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/read-output.d.ts +0 -9
- package/dist/modes/interactive/components/read-output.d.ts.map +1 -1
- package/dist/modes/interactive/components/read-output.js +0 -20
- package/dist/modes/interactive/components/read-output.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.d.ts +0 -17
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +2 -2
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +4 -13
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/login-controller.d.ts +1 -1
- package/dist/modes/interactive/login-controller.d.ts.map +1 -1
- package/dist/modes/interactive/login-controller.js +1 -1
- package/dist/modes/interactive/login-controller.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +0 -20
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +7 -7
- package/dist/modes/interactive/resource-display.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +0 -4
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +0 -7
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +0 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +0 -1
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js +3 -0
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts +0 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/changelog.d.ts +0 -4
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +1 -1
- package/dist/utils/changelog.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +1 -1
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/paths.d.ts +0 -2
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/plan/parser.d.ts +5 -4
- package/dist/utils/plan/parser.d.ts.map +1 -1
- package/dist/utils/plan/parser.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +0 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -7
- package/dist/core/index.d.ts +0 -12
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -12
- package/dist/core/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA2IH;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAoC,EAAU;IACzE,IAAI,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IAClC,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;AAAA,CACxD;AAID,MAAM,SAAS;IACN,KAAK,GAAW,EAAE,CAAC;IACnB,UAAU,GAAgB,EAAE,CAAC;IAC7B,MAAM,GAAG,CAAC,CAAC;IACF,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;IACzC,UAAU,GAAG,CAAC,CAAC;IACf,aAAa,GAAG,CAAC,CAAC;IAE1B;;;;OAIG;IACH,OAAO,GAAW;QACjB,OAAO,IAAI,CAAC,aAAa,CAAC;IAAA,CAC1B;IAED;;;;OAIG;IACH,KAAK,CAAC,EAAc,EAAQ;QAC3B,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC;YACJ,EAAE,EAAE,CAAC;QACN,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC;gBAAE,IAAI,CAAC,IAAI,EAAE,CAAC;QACxC,CAAC;IAAA,CACD;IAED,MAAM,CAAC,KAAa,EAAE,OAAO,GAAsB,EAAE,EAAQ;QAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,IAAI,GAAS;YAClB,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE;YACjB,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,iBAAiB;YACxC,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACd,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,MAAM,CAAC,EAAU,EAAE,KAAgB,EAAQ;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,wCAAwC,EAAE,EAAE,CAAC,CAAC;YAC3D,OAAO;QACR,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC3D,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC3D,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QAC7E,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QAC7E,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QAC7E,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;QAC1E,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,4EAA4E;QAC5E,6EAA2E;QAC3E,gDAAgD;QAChD,IAAI,MAAM,IAAI,KAAK;YAAE,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QAC5C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED;;;;OAIG;IACH,WAAW,CAAC,KAAyE,EAAa;QACjG,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC,CAAC;QAChE,IAAI,QAAQ,EAAE,CAAC;YACd,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YACtC,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,OAAO,QAAQ,CAAC;QACjB,CAAC;QACD,MAAM,OAAO,GAAc;YAC1B,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,KAAK,EAAE,KAAK,CAAC,KAAK;SAClB,CAAC;QACF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC;IAAA,CACf;IAED,iFAA+E;IAC/E,UAAU,CAAC,EAAU,EAAE,KAAqB,EAAQ;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,8EAA8E;IAC9E,aAAa,CAAC,EAAU,EAAE,KAAyD,EAAQ;QAC1F,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,GAAG,CAAC,CAAC;YACrE,OAAO;QACR,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QAChC,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;QAC9B,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,MAAM,GAAyB;QAC9B,OAAO,IAAI,CAAC,UAAU,CAAC;IAAA,CACvB;IAEO,eAAe,CAAC,KAAgB,EAAE,KAAqB,EAAQ;QACtE,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtD,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACzD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;YAAE,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAClE,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;YAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC/D,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAAA,CACzD;IAED,MAAM,CAAC,EAAU,EAAQ;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED;;;;;;OAMG;IACH,OAAO,CAAC,GAAsB,EAAQ;QACrC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAa,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;QACxF,IAAI,OAAO,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM;YAAE,OAAO;QAC1C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM;YAAE,OAAO;QAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;QACrC,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED;;;;;;;;;;;;;OAaG;IACH,KAAK,GAAS;QACb,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC;QAC9F,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACrE,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;QACpB,0EAA0E;QAC1E,2EAAyE;QACzE,wEAAwE;QACxE,sEAAsE;QACtE,MAAM,QAAQ,GAAG,CAAC,CAAY,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QAC5G,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;YAChB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACvF,CAAC;aAAM,CAAC;YACP,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;YACpD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CACvC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,CACrE,CAAC;QACH,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,IAAI,GAAoB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAED,8EAA8E;IAC9E,KAAK,GAAS;QACb,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,SAAS,CAAC,QAAkB,EAAc;QACzC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC;YACZ,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAA,CAChC,CAAC;IAAA,CACF;IAEO,IAAI,GAAS;QACpB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC;YAAE,OAAO;QAChC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACvC,QAAQ,EAAE,CAAC;QACZ,CAAC;IAAA,CACD;CACD;AAED,uCAAuC;AACvC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\n/**\n * `cancelled` is a user-initiated stop (Esc/abort mid-run) — deliberately\n * distinct from `failed` so the panel doesn't paint an intentional interrupt\n * as an error.\n */\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\" | \"cancelled\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\" | \"cancelled\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/**\n\t * Live activity descriptor for a running subagent (e.g. the tool it is\n\t * currently executing), fed by the pool's `task_progress` events. Empty string\n\t * means \"no current activity\" — `applyAgentPatch` skips `undefined`, so callers\n\t * clear it by patching `\"\"`, not `undefined`.\n\t */\n\tactivity?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Id of the task that spawned this one, linking a dispatched subagent (and its\n\t * own delegations) back to the Task call that created it. Root tasks omit it.\n\t * Drives the subagents lens's recursive task tree: a node's children are the\n\t * tasks whose `parentTaskId` is its id, so nesting deeper than one level (a\n\t * subagent that spawns a subagent) is visible. Set when a child subagent's task\n\t * subtree is merged into the parent (see finalizeDispatchResult).\n\t */\n\tparentTaskId?: number;\n\t/**\n\t * Id of the main-agent (TodoWrite) task this run is working on, set at\n\t * dispatch time when exactly one plan item is in_progress. Purely a display\n\t * link: the flat (\"tasks\") lens nests the run under its plan item so the\n\t * plan and the agents executing it read as one picture. Deliberately NOT\n\t * `parentTaskId` — that field drives the subagents tree and the\n\t * cross-process subtree merge, and overloading it would move runs between\n\t * lenses. A dangling link (the todo was removed/replaced) is simply not\n\t * rendered.\n\t */\n\tlinkedTaskId?: number;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\t/**\n\t * Canonical TodoWrite item content for main-plan tasks. `title` is the\n\t * *display* form and legitimately changes between `content` and `activeForm`\n\t * as the item's status moves, so it cannot identify an item across TodoWrite\n\t * calls. This field can: the tool reconciles incoming items against it by\n\t * identity first (position only as a fallback), keeping task ids — and the\n\t * subagent runs linked to them — attached to the same plan item when the\n\t * list is reordered or shrunk.\n\t */\n\ttodoContent?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n\tparentTaskId?: number;\n\tlinkedTaskId?: number;\n}\n\nexport type TaskPatch = Partial<\n\tPick<\n\t\tTask,\n\t\t| \"title\"\n\t\t| \"status\"\n\t\t| \"source\"\n\t\t| \"subagentMode\"\n\t\t| \"agent\"\n\t\t| \"usage\"\n\t\t| \"note\"\n\t\t| \"parentTaskId\"\n\t\t| \"linkedTaskId\"\n\t\t| \"todoContent\"\n\t>\n>;\n\nexport type TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\tprivate batchDepth = 0;\n\tprivate mutationCount = 0;\n\n\t/**\n\t * Monotonic counter bumped on every mutation (including those inside a\n\t * batch). Lets renderers cache derived state and invalidate it cheaply by\n\t * comparing versions instead of deep-diffing tasks/agents.\n\t */\n\tversion(): number {\n\t\treturn this.mutationCount;\n\t}\n\n\t/**\n\t * Run a series of mutations without notifying listeners until the batch\n\t * completes, so the TUI renders once instead of per-item. Nested batches are\n\t * supported: only the outermost batch flushes.\n\t */\n\tbatch(fn: () => void): void {\n\t\tthis.batchDepth++;\n\t\ttry {\n\t\t\tfn();\n\t\t} finally {\n\t\t\tthis.batchDepth--;\n\t\t\tif (this.batchDepth === 0) this.emit();\n\t\t}\n\t}\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tparentTaskId: options.parentTaskId,\n\t\t\tlinkedTaskId: options.linkedTaskId,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.parentTaskId !== undefined) task.parentTaskId = patch.parentTaskId;\n\t\tif (patch.linkedTaskId !== undefined) task.linkedTaskId = patch.linkedTaskId;\n\t\tif (patch.todoContent !== undefined) task.todoContent = patch.todoContent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\t// `note` is clearable: passing `note: undefined` explicitly removes a stale\n\t\t// ⚠ cue (e.g. a warning from a previous run of the same task row). Callers\n\t\t// that don't mention `note` leave it untouched.\n\t\tif (\"note\" in patch) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tactivity: agent.activity,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.activity !== undefined) agent.activity = patch.activity;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Arrange the given tasks (by id) into the specified relative order, keeping\n\t * every other task fixed: the matched tasks permute among their existing\n\t * array slots. Lets TodoWrite render the plan in list order while ids stay\n\t * pinned to their items (its identity-based reconcile). Ignored unless every\n\t * id resolves to a task.\n\t */\n\tarrange(ids: readonly number[]): void {\n\t\tconst byId = new Map(this.tasks.map((t) => [t.id, t]));\n\t\tconst ordered = ids.map((id) => byId.get(id)).filter((t): t is Task => t !== undefined);\n\t\tif (ordered.length !== ids.length) return;\n\t\tconst idSet = new Set(ids);\n\t\tconst slots: number[] = [];\n\t\tfor (let i = 0; i < this.tasks.length; i++) {\n\t\t\tif (idSet.has(this.tasks[i]!.id)) slots.push(i);\n\t\t}\n\t\tif (slots.length !== ordered.length) return;\n\t\tfor (let k = 0; k < slots.length; k++) {\n\t\t\tthis.tasks[slots[k]!] = ordered[k]!;\n\t\t}\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\t// Subagent rows are per-run (one roster entry per dispatch), so a settled\n\t\t// run whose task was just dropped is dead weight — keeping it would grow\n\t\t// the roster without bound across turns. Role/main agents with non-zero\n\t\t// accumulated stats survive so cross-turn team cost accounting holds.\n\t\tconst hasStats = (a: TaskAgent) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0);\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\tthis.taskAgents = this.taskAgents.filter((a) => a.kind !== \"subagent\" && hasStats(a));\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.kind !== \"subagent\" && hasStats(a)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tthis.mutationCount++;\n\t\tif (this.batchDepth > 0) return;\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
1
|
+
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\n/**\n * `cancelled` is a user-initiated stop (Esc/abort mid-run) — deliberately\n * distinct from `failed` so the panel doesn't paint an intentional interrupt\n * as an error.\n */\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\" | \"cancelled\";\n\n/**\n * What kind of background work owns a task, surfaced as a source glyph in the\n * pane. Unset means the main agent. A \"team\" origin is reserved for the\n * hooteams integration and stays unwired until that lands.\n */\nexport type TaskSource = \"subagent\" | \"mcp\";\n\n/**\n * Kind of agent that can own tasks in the pane's grouped views:\n * the main session (orchestrator), a spawned subagent, or a named\n * team role-agent (e.g. fed by a hooteams bridge).\n */\nexport type TaskAgentKind = \"main\" | \"subagent\" | \"role\";\n\n/** Lifecycle word shown as the agent's `[state]` tag in grouped views. */\nexport type TaskAgentState = \"active\" | \"running\" | \"done\" | \"queued\" | \"idle\" | \"waiting\" | \"failed\" | \"cancelled\";\n\n/**\n * An agent that owns tasks, rendered as a group header in the pane's\n * `subagents` / `teams` views. Subagent dispatches register themselves here;\n * external orchestrators (hooteams) can upsert role-agents with handoffs.\n */\nexport interface TaskAgent {\n\treadonly id: string;\n\tname: string;\n\t/** Short descriptor after the name: \"orchestrator\", \"subagent\", or a team role like \"architect\". */\n\trole?: string;\n\tkind: TaskAgentKind;\n\tstate?: TaskAgentState;\n\t/** Handoff arrow text for team views (e.g. \"→ reviewer\", \"← builder\"). */\n\thandoff?: string;\n\t/**\n\t * Live activity descriptor for a running subagent (e.g. the tool it is\n\t * currently executing), fed by the pool's `task_progress` events. Empty string\n\t * means \"no current activity\" — `applyAgentPatch` skips `undefined`, so callers\n\t * clear it by patching `\"\"`, not `undefined`.\n\t */\n\tactivity?: string;\n\t/** Per-agent token + cost totals, shown right-aligned on the group header. */\n\tstats?: { input: number; output: number; cost: number };\n}\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent delegation vs MCP tool call; unset = main agent); drives the pane's source glyph. */\n\tsource?: TaskSource;\n\t/**\n\t * Origin label shown as the row's `[tag]` in the task pane: the subagent\n\t * type for delegations (e.g. \"explore\"), the MCP server name for MCP tasks\n\t * (e.g. \"github\").\n\t */\n\tsubagentMode?: string;\n\t/** Id of the owning TaskAgent; drives grouping in the pane's subagents/teams views. */\n\tagent?: string;\n\t/**\n\t * Id of the task that spawned this one, linking a dispatched subagent (and its\n\t * own delegations) back to the Task call that created it. Root tasks omit it.\n\t * Drives the subagents lens's recursive task tree: a node's children are the\n\t * tasks whose `parentTaskId` is its id, so nesting deeper than one level (a\n\t * subagent that spawns a subagent) is visible. Set when a child subagent's task\n\t * subtree is merged into the parent (see finalizeDispatchResult).\n\t */\n\tparentTaskId?: number;\n\t/**\n\t * Id of the main-agent (TodoWrite) task this run is working on, set at\n\t * dispatch time when exactly one plan item is in_progress. Purely a display\n\t * link: the flat (\"tasks\") lens nests the run under its plan item so the\n\t * plan and the agents executing it read as one picture. Deliberately NOT\n\t * `parentTaskId` — that field drives the subagents tree and the\n\t * cross-process subtree merge, and overloading it would move runs between\n\t * lenses. A dangling link (the todo was removed/replaced) is simply not\n\t * rendered.\n\t */\n\tlinkedTaskId?: number;\n\t/**\n\t * Short warning note surfaced as a ⚠ cue in the task pane (e.g. the subagent\n\t * fell back to the inherited model, or was skipped because the provider was\n\t * exhausted). Kept terse so it fits the row's right column.\n\t */\n\tnote?: string;\n\t/**\n\t * Canonical TodoWrite item content for main-plan tasks. `title` is the\n\t * *display* form and legitimately changes between `content` and `activeForm`\n\t * as the item's status moves, so it cannot identify an item across TodoWrite\n\t * calls. This field can: the tool reconciles incoming items against it by\n\t * identity first (position only as a fallback), keeping task ids — and the\n\t * subagent runs linked to them — attached to the same plan item when the\n\t * list is reordered or shrunk.\n\t */\n\ttodoContent?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\ninterface CreateTaskOptions {\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tagent?: string;\n\tparentTaskId?: number;\n\tlinkedTaskId?: number;\n}\n\ntype TaskPatch = Partial<\n\tPick<\n\t\tTask,\n\t\t| \"title\"\n\t\t| \"status\"\n\t\t| \"source\"\n\t\t| \"subagentMode\"\n\t\t| \"agent\"\n\t\t| \"usage\"\n\t\t| \"note\"\n\t\t| \"parentTaskId\"\n\t\t| \"linkedTaskId\"\n\t\t| \"todoContent\"\n\t>\n>;\n\ntype TaskAgentPatch = Partial<Omit<TaskAgent, \"id\">>;\n\n/**\n * Owner group for a task when no explicit agent is set: subagent-sourced work\n * falls into a generic \"subagent\" group, everything else belongs to main.\n * Shared by the store's reset() and the pane's grouped views so the two never\n * disagree about which agents still own live tasks.\n */\nexport function taskOwnerId(task: Pick<Task, \"agent\" | \"source\">): string {\n\tif (task.agent) return task.agent;\n\treturn task.source === \"subagent\" ? \"subagent\" : \"main\";\n}\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate taskAgents: TaskAgent[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\tprivate batchDepth = 0;\n\tprivate mutationCount = 0;\n\n\t/**\n\t * Monotonic counter bumped on every mutation (including those inside a\n\t * batch). Lets renderers cache derived state and invalidate it cheaply by\n\t * comparing versions instead of deep-diffing tasks/agents.\n\t */\n\tversion(): number {\n\t\treturn this.mutationCount;\n\t}\n\n\t/**\n\t * Run a series of mutations without notifying listeners until the batch\n\t * completes, so the TUI renders once instead of per-item. Nested batches are\n\t * supported: only the outermost batch flushes.\n\t */\n\tbatch(fn: () => void): void {\n\t\tthis.batchDepth++;\n\t\ttry {\n\t\t\tfn();\n\t\t} finally {\n\t\t\tthis.batchDepth--;\n\t\t\tif (this.batchDepth === 0) this.emit();\n\t\t}\n\t}\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsource: options.source,\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tagent: options.agent,\n\t\t\tparentTaskId: options.parentTaskId,\n\t\t\tlinkedTaskId: options.linkedTaskId,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) {\n\t\t\tconsole.warn(`[task-store] update: unknown task id ${id}`);\n\t\t\treturn;\n\t\t}\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.source !== undefined) task.source = patch.source;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.agent !== undefined) task.agent = patch.agent;\n\t\tif (patch.parentTaskId !== undefined) task.parentTaskId = patch.parentTaskId;\n\t\tif (patch.linkedTaskId !== undefined) task.linkedTaskId = patch.linkedTaskId;\n\t\tif (patch.todoContent !== undefined) task.todoContent = patch.todoContent;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\t// `note` is clearable: passing `note: undefined` explicitly removes a stale\n\t\t// ⚠ cue (e.g. a warning from a previous run of the same task row). Callers\n\t\t// that don't mention `note` leave it untouched.\n\t\tif (\"note\" in patch) task.note = patch.note;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Register or update an agent for the grouped task views. Merges into an\n\t * existing entry with the same id (accumulated stats survive a re-dispatch);\n\t * creates it otherwise.\n\t */\n\tupsertAgent(agent: { id: string } & TaskAgentPatch & Pick<TaskAgent, \"name\" | \"kind\">): TaskAgent {\n\t\tconst existing = this.taskAgents.find((a) => a.id === agent.id);\n\t\tif (existing) {\n\t\t\tthis.applyAgentPatch(existing, agent);\n\t\t\tthis.emit();\n\t\t\treturn existing;\n\t\t}\n\t\tconst created: TaskAgent = {\n\t\t\tid: agent.id,\n\t\t\tname: agent.name,\n\t\t\trole: agent.role,\n\t\t\tkind: agent.kind,\n\t\t\tstate: agent.state,\n\t\t\thandoff: agent.handoff,\n\t\t\tactivity: agent.activity,\n\t\t\tstats: agent.stats,\n\t\t};\n\t\tthis.taskAgents.push(created);\n\t\tthis.emit();\n\t\treturn created;\n\t}\n\n\t/** Patch an existing agent (state/handoff/stats…). Unknown ids are ignored. */\n\tpatchAgent(id: string, patch: TaskAgentPatch): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) return;\n\t\tthis.applyAgentPatch(agent, patch);\n\t\tthis.emit();\n\t}\n\n\t/** Add a usage delta to an agent's running totals (creating them at zero). */\n\taddAgentStats(id: string, delta: { input?: number; output?: number; cost?: number }): void {\n\t\tconst agent = this.taskAgents.find((a) => a.id === id);\n\t\tif (!agent) {\n\t\t\tconsole.warn(`[task-store] addAgentStats: unknown agent id \"${id}\"`);\n\t\t\treturn;\n\t\t}\n\t\tconst stats = agent.stats ?? { input: 0, output: 0, cost: 0 };\n\t\tstats.input += delta.input ?? 0;\n\t\tstats.output += delta.output ?? 0;\n\t\tstats.cost += delta.cost ?? 0;\n\t\tagent.stats = stats;\n\t\tthis.emit();\n\t}\n\n\tagents(): readonly TaskAgent[] {\n\t\treturn this.taskAgents;\n\t}\n\n\tprivate applyAgentPatch(agent: TaskAgent, patch: TaskAgentPatch): void {\n\t\tif (patch.name !== undefined) agent.name = patch.name;\n\t\tif (patch.role !== undefined) agent.role = patch.role;\n\t\tif (patch.kind !== undefined) agent.kind = patch.kind;\n\t\tif (patch.state !== undefined) agent.state = patch.state;\n\t\tif (patch.activity !== undefined) agent.activity = patch.activity;\n\t\tif (patch.handoff !== undefined) agent.handoff = patch.handoff;\n\t\tif (patch.stats !== undefined) agent.stats = patch.stats;\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Arrange the given tasks (by id) into the specified relative order, keeping\n\t * every other task fixed: the matched tasks permute among their existing\n\t * array slots. Lets TodoWrite render the plan in list order while ids stay\n\t * pinned to their items (its identity-based reconcile). Ignored unless every\n\t * id resolves to a task.\n\t */\n\tarrange(ids: readonly number[]): void {\n\t\tconst byId = new Map(this.tasks.map((t) => [t.id, t]));\n\t\tconst ordered = ids.map((id) => byId.get(id)).filter((t): t is Task => t !== undefined);\n\t\tif (ordered.length !== ids.length) return;\n\t\tconst idSet = new Set(ids);\n\t\tconst slots: number[] = [];\n\t\tfor (let i = 0; i < this.tasks.length; i++) {\n\t\t\tif (idSet.has(this.tasks[i]!.id)) slots.push(i);\n\t\t}\n\t\tif (slots.length !== ordered.length) return;\n\t\tfor (let k = 0; k < slots.length; k++) {\n\t\t\tthis.tasks[slots[k]!] = ordered[k]!;\n\t\t}\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t * Agents with accumulated stats are preserved across resets so cross-turn cost\n\t * accounting survives.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\t// Subagent rows are per-run (one roster entry per dispatch), so a settled\n\t\t// run whose task was just dropped is dead weight — keeping it would grow\n\t\t// the roster without bound across turns. Role/main agents with non-zero\n\t\t// accumulated stats survive so cross-turn team cost accounting holds.\n\t\tconst hasStats = (a: TaskAgent) => a.stats && (a.stats.input > 0 || a.stats.output > 0 || a.stats.cost > 0);\n\t\tif (active.length === 0) {\n\t\t\tthis.nextId = 1;\n\t\t\tthis.taskAgents = this.taskAgents.filter((a) => a.kind !== \"subagent\" && hasStats(a));\n\t\t} else {\n\t\t\tconst liveOwners = new Set(active.map(taskOwnerId));\n\t\t\tthis.taskAgents = this.taskAgents.filter(\n\t\t\t\t(a) => liveOwners.has(a.id) || (a.kind !== \"subagent\" && hasStats(a)),\n\t\t\t);\n\t\t}\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.taskAgents = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tthis.mutationCount++;\n\t\tif (this.batchDepth > 0) return;\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
package/dist/core/team-auto.d.ts
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
* signalled, via a process "exit" hook (the interactive shutdown path calls
|
|
10
10
|
* process.exit directly, so an async cleanup would never run).
|
|
11
11
|
*/
|
|
12
|
-
/** Config locations probed at each directory level, in priority order. */
|
|
13
|
-
export declare const TEAM_CONFIG_CANDIDATES: string[];
|
|
14
12
|
/**
|
|
15
13
|
* Walk up from startDir to the filesystem root, returning the first config
|
|
16
14
|
* found. Both candidates are probed per level (.agents/teams/default.json
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-auto.d.ts","sourceRoot":"","sources":["../../src/core/team-auto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"team-auto.d.ts","sourceRoot":"","sources":["../../src/core/team-auto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAWnE;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAc9C;AAmBD,mEAAmE;AACnE,wBAAgB,uBAAuB,CACtC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAClC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAIvD;AAED,MAAM,WAAW,QAAQ;IACxB,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC/B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,sDAAsD;IACtD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAkCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoDjG","sourcesContent":["/**\n * `--team auto`: discover a team config, spawn a local hooteams server as a\n * child process, and hand back its URL so the rest of the pipeline behaves\n * exactly as if `--team http://localhost:<port>` had been passed.\n *\n * hooteams is intentionally not bundled — the launcher is resolved from PATH\n * (`hooteams`, falling back to `bunx hooteams`) and missing pieces fail with\n * a clear, actionable error. The child is reaped on hoocode exit, clean or\n * signalled, via a process \"exit\" hook (the interactive shutdown path calls\n * process.exit directly, so an async cleanup would never run).\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { accessSync, constants, existsSync } from \"node:fs\";\nimport { createServer } from \"node:net\";\nimport path from \"node:path\";\n\n/** Config locations probed at each directory level, in priority order. */\nconst TEAM_CONFIG_CANDIDATES = [path.join(\".agents\", \"teams\", \"default.json\"), \"hooteams.config.json\"];\n\n/**\n * Walk up from startDir to the filesystem root, returning the first config\n * found. Both candidates are probed per level (.agents/teams/default.json\n * wins over hooteams.config.json in the same directory).\n */\nexport function findTeamConfig(startDir: string): string | undefined {\n\tlet dir = path.resolve(startDir);\n\twhile (true) {\n\t\tfor (const candidate of TEAM_CONFIG_CANDIDATES) {\n\t\t\tconst candidatePath = path.join(dir, candidate);\n\t\t\tif (existsSync(candidatePath)) return candidatePath;\n\t\t}\n\t\tconst parent = path.dirname(dir);\n\t\tif (parent === dir) return undefined;\n\t\tdir = parent;\n\t}\n}\n\n/** Ask the OS for a free port by binding port 0 and reading the assignment. */\nexport function findFreePort(): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = createServer();\n\t\tserver.once(\"error\", reject);\n\t\tserver.listen(0, \"127.0.0.1\", () => {\n\t\t\tconst address = server.address();\n\t\t\tif (address === null || typeof address === \"string\") {\n\t\t\t\tserver.close(() => reject(new Error(\"could not determine a free port\")));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { port } = address;\n\t\t\tserver.close(() => resolve(port));\n\t\t});\n\t});\n}\n\nfunction isExecutableOnPath(name: string, env: NodeJS.ProcessEnv): boolean {\n\tconst pathVar = env.PATH ?? \"\";\n\tconst extensions = process.platform === \"win32\" ? (env.PATHEXT ?? \".EXE;.CMD;.BAT;.COM\").split(\";\") : [\"\"];\n\tfor (const dir of pathVar.split(path.delimiter)) {\n\t\tif (!dir) continue;\n\t\tfor (const extension of extensions) {\n\t\t\ttry {\n\t\t\t\taccessSync(path.join(dir, name + extension.toLowerCase()), constants.X_OK);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\t// keep probing\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\n/** How to launch hooteams: directly from PATH, or through bunx. */\nexport function resolveHooteamsLauncher(\n\tenv: NodeJS.ProcessEnv = process.env,\n): { command: string; prefixArgs: string[] } | undefined {\n\tif (isExecutableOnPath(\"hooteams\", env)) return { command: \"hooteams\", prefixArgs: [] };\n\tif (isExecutableOnPath(\"bunx\", env)) return { command: \"bunx\", prefixArgs: [\"hooteams\"] };\n\treturn undefined;\n}\n\nexport interface AutoTeam {\n\t/** Base URL of the spawned hooteams server. */\n\turl: string;\n\t/** Graceful shutdown: POST /stop, then kill the child's process group. */\n\tstop(): Promise<void>;\n}\n\nexport interface AutoTeamOptions {\n\t/** Startup progress sink (pre-TUI, so console is fine). */\n\tlog?: (message: string) => void;\n\t/** How long to wait for GET /health (default 15s). */\n\thealthTimeoutMs?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nfunction killChild(child: ChildProcess): void {\n\tif (child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return;\n\ttry {\n\t\t// POSIX: the child leads its own process group (detached), so a negative\n\t\t// pid reaches hooteams even when launched through a bunx wrapper.\n\t\tif (process.platform !== \"win32\") process.kill(-child.pid, \"SIGTERM\");\n\t\telse child.kill();\n\t} catch {\n\t\t// Already gone.\n\t}\n}\n\nasync function waitForHealth(url: string, child: ChildProcess, timeoutMs: number): Promise<void> {\n\tconst deadline = Date.now() + timeoutMs;\n\twhile (Date.now() < deadline) {\n\t\tif (child.exitCode !== null || child.signalCode !== null) {\n\t\t\tthrow new Error(`--team auto: hooteams exited (code ${child.exitCode ?? \"signal\"}) before becoming healthy`);\n\t\t}\n\t\ttry {\n\t\t\tconst response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(1000) });\n\t\t\tif (response.ok) {\n\t\t\t\tconst body = (await response.json()) as { ok?: boolean };\n\t\t\t\tif (body.ok === true) return;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not up yet; keep polling.\n\t\t}\n\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\n\t}\n\tthrow new Error(`--team auto: hooteams did not report healthy at ${url}/health within ${timeoutMs}ms`);\n}\n\n/**\n * Resolve the config, spawn hooteams on a free port, and wait for /health.\n * Throws (with a message ready for the terminal) when no config is found, no\n * launcher resolves, or the server never becomes healthy.\n */\nexport async function startAutoTeam(cwd: string, options: AutoTeamOptions = {}): Promise<AutoTeam> {\n\tconst env = options.env ?? process.env;\n\tconst config = findTeamConfig(cwd);\n\tif (!config) {\n\t\tthrow new Error(\n\t\t\t`--team auto: no team config found. Looked for ${TEAM_CONFIG_CANDIDATES.join(\" or \")} in ${cwd} and every parent directory.`,\n\t\t);\n\t}\n\tconst launcher = resolveHooteamsLauncher(env);\n\tif (!launcher) {\n\t\tthrow new Error(\n\t\t\t\"--team auto: hooteams is not on PATH and bunx is unavailable. Install hooteams (or bun) or pass --team <url> to use a running server.\",\n\t\t);\n\t}\n\n\tconst port = await findFreePort();\n\tconst url = `http://localhost:${port}`;\n\toptions.log?.(`Starting hooteams (config ${config}) on port ${port}…`);\n\n\tconst child = spawn(\n\t\tlauncher.command,\n\t\t[...launcher.prefixArgs, \"start\", \"--config\", config, \"--port\", String(port)],\n\t\t{\n\t\t\tstdio: \"ignore\",\n\t\t\tenv,\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t},\n\t);\n\tchild.unref();\n\tconst reapOnExit = () => killChild(child);\n\tprocess.on(\"exit\", reapOnExit);\n\n\ttry {\n\t\tawait waitForHealth(url, child, options.healthTimeoutMs ?? 15000);\n\t} catch (error) {\n\t\tprocess.off(\"exit\", reapOnExit);\n\t\tkillChild(child);\n\t\tthrow error;\n\t}\n\n\treturn {\n\t\turl,\n\t\tasync stop() {\n\t\t\tprocess.off(\"exit\", reapOnExit);\n\t\t\ttry {\n\t\t\t\tawait fetch(`${url}/stop`, { method: \"POST\", signal: AbortSignal.timeout(2000) });\n\t\t\t} catch {\n\t\t\t\t// Graceful stop is best-effort; the kill below is the guarantee.\n\t\t\t}\n\t\t\tkillChild(child);\n\t\t},\n\t};\n}\n"]}
|
package/dist/core/team-auto.js
CHANGED
|
@@ -14,7 +14,7 @@ import { accessSync, constants, existsSync } from "node:fs";
|
|
|
14
14
|
import { createServer } from "node:net";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
/** Config locations probed at each directory level, in priority order. */
|
|
17
|
-
|
|
17
|
+
const TEAM_CONFIG_CANDIDATES = [path.join(".agents", "teams", "default.json"), "hooteams.config.json"];
|
|
18
18
|
/**
|
|
19
19
|
* Walk up from startDir to the filesystem root, returning the first config
|
|
20
20
|
* found. Both candidates are probed per level (.agents/teams/default.json
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-auto.js","sourceRoot":"","sources":["../../src/core/team-auto.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `--team auto`: discover a team config, spawn a local hooteams server as a\n * child process, and hand back its URL so the rest of the pipeline behaves\n * exactly as if `--team http://localhost:<port>` had been passed.\n *\n * hooteams is intentionally not bundled — the launcher is resolved from PATH\n * (`hooteams`, falling back to `bunx hooteams`) and missing pieces fail with\n * a clear, actionable error. The child is reaped on hoocode exit, clean or\n * signalled, via a process \"exit\" hook (the interactive shutdown path calls\n * process.exit directly, so an async cleanup would never run).\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { accessSync, constants, existsSync } from \"node:fs\";\nimport { createServer } from \"node:net\";\nimport path from \"node:path\";\n\n/** Config locations probed at each directory level, in priority order. */\nexport const TEAM_CONFIG_CANDIDATES = [path.join(\".agents\", \"teams\", \"default.json\"), \"hooteams.config.json\"];\n\n/**\n * Walk up from startDir to the filesystem root, returning the first config\n * found. Both candidates are probed per level (.agents/teams/default.json\n * wins over hooteams.config.json in the same directory).\n */\nexport function findTeamConfig(startDir: string): string | undefined {\n\tlet dir = path.resolve(startDir);\n\twhile (true) {\n\t\tfor (const candidate of TEAM_CONFIG_CANDIDATES) {\n\t\t\tconst candidatePath = path.join(dir, candidate);\n\t\t\tif (existsSync(candidatePath)) return candidatePath;\n\t\t}\n\t\tconst parent = path.dirname(dir);\n\t\tif (parent === dir) return undefined;\n\t\tdir = parent;\n\t}\n}\n\n/** Ask the OS for a free port by binding port 0 and reading the assignment. */\nexport function findFreePort(): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = createServer();\n\t\tserver.once(\"error\", reject);\n\t\tserver.listen(0, \"127.0.0.1\", () => {\n\t\t\tconst address = server.address();\n\t\t\tif (address === null || typeof address === \"string\") {\n\t\t\t\tserver.close(() => reject(new Error(\"could not determine a free port\")));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { port } = address;\n\t\t\tserver.close(() => resolve(port));\n\t\t});\n\t});\n}\n\nfunction isExecutableOnPath(name: string, env: NodeJS.ProcessEnv): boolean {\n\tconst pathVar = env.PATH ?? \"\";\n\tconst extensions = process.platform === \"win32\" ? (env.PATHEXT ?? \".EXE;.CMD;.BAT;.COM\").split(\";\") : [\"\"];\n\tfor (const dir of pathVar.split(path.delimiter)) {\n\t\tif (!dir) continue;\n\t\tfor (const extension of extensions) {\n\t\t\ttry {\n\t\t\t\taccessSync(path.join(dir, name + extension.toLowerCase()), constants.X_OK);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\t// keep probing\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\n/** How to launch hooteams: directly from PATH, or through bunx. */\nexport function resolveHooteamsLauncher(\n\tenv: NodeJS.ProcessEnv = process.env,\n): { command: string; prefixArgs: string[] } | undefined {\n\tif (isExecutableOnPath(\"hooteams\", env)) return { command: \"hooteams\", prefixArgs: [] };\n\tif (isExecutableOnPath(\"bunx\", env)) return { command: \"bunx\", prefixArgs: [\"hooteams\"] };\n\treturn undefined;\n}\n\nexport interface AutoTeam {\n\t/** Base URL of the spawned hooteams server. */\n\turl: string;\n\t/** Graceful shutdown: POST /stop, then kill the child's process group. */\n\tstop(): Promise<void>;\n}\n\nexport interface AutoTeamOptions {\n\t/** Startup progress sink (pre-TUI, so console is fine). */\n\tlog?: (message: string) => void;\n\t/** How long to wait for GET /health (default 15s). */\n\thealthTimeoutMs?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nfunction killChild(child: ChildProcess): void {\n\tif (child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return;\n\ttry {\n\t\t// POSIX: the child leads its own process group (detached), so a negative\n\t\t// pid reaches hooteams even when launched through a bunx wrapper.\n\t\tif (process.platform !== \"win32\") process.kill(-child.pid, \"SIGTERM\");\n\t\telse child.kill();\n\t} catch {\n\t\t// Already gone.\n\t}\n}\n\nasync function waitForHealth(url: string, child: ChildProcess, timeoutMs: number): Promise<void> {\n\tconst deadline = Date.now() + timeoutMs;\n\twhile (Date.now() < deadline) {\n\t\tif (child.exitCode !== null || child.signalCode !== null) {\n\t\t\tthrow new Error(`--team auto: hooteams exited (code ${child.exitCode ?? \"signal\"}) before becoming healthy`);\n\t\t}\n\t\ttry {\n\t\t\tconst response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(1000) });\n\t\t\tif (response.ok) {\n\t\t\t\tconst body = (await response.json()) as { ok?: boolean };\n\t\t\t\tif (body.ok === true) return;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not up yet; keep polling.\n\t\t}\n\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\n\t}\n\tthrow new Error(`--team auto: hooteams did not report healthy at ${url}/health within ${timeoutMs}ms`);\n}\n\n/**\n * Resolve the config, spawn hooteams on a free port, and wait for /health.\n * Throws (with a message ready for the terminal) when no config is found, no\n * launcher resolves, or the server never becomes healthy.\n */\nexport async function startAutoTeam(cwd: string, options: AutoTeamOptions = {}): Promise<AutoTeam> {\n\tconst env = options.env ?? process.env;\n\tconst config = findTeamConfig(cwd);\n\tif (!config) {\n\t\tthrow new Error(\n\t\t\t`--team auto: no team config found. Looked for ${TEAM_CONFIG_CANDIDATES.join(\" or \")} in ${cwd} and every parent directory.`,\n\t\t);\n\t}\n\tconst launcher = resolveHooteamsLauncher(env);\n\tif (!launcher) {\n\t\tthrow new Error(\n\t\t\t\"--team auto: hooteams is not on PATH and bunx is unavailable. Install hooteams (or bun) or pass --team <url> to use a running server.\",\n\t\t);\n\t}\n\n\tconst port = await findFreePort();\n\tconst url = `http://localhost:${port}`;\n\toptions.log?.(`Starting hooteams (config ${config}) on port ${port}…`);\n\n\tconst child = spawn(\n\t\tlauncher.command,\n\t\t[...launcher.prefixArgs, \"start\", \"--config\", config, \"--port\", String(port)],\n\t\t{\n\t\t\tstdio: \"ignore\",\n\t\t\tenv,\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t},\n\t);\n\tchild.unref();\n\tconst reapOnExit = () => killChild(child);\n\tprocess.on(\"exit\", reapOnExit);\n\n\ttry {\n\t\tawait waitForHealth(url, child, options.healthTimeoutMs ?? 15000);\n\t} catch (error) {\n\t\tprocess.off(\"exit\", reapOnExit);\n\t\tkillChild(child);\n\t\tthrow error;\n\t}\n\n\treturn {\n\t\turl,\n\t\tasync stop() {\n\t\t\tprocess.off(\"exit\", reapOnExit);\n\t\t\ttry {\n\t\t\t\tawait fetch(`${url}/stop`, { method: \"POST\", signal: AbortSignal.timeout(2000) });\n\t\t\t} catch {\n\t\t\t\t// Graceful stop is best-effort; the kill below is the guarantee.\n\t\t\t}\n\t\t\tkillChild(child);\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"team-auto.js","sourceRoot":"","sources":["../../src/core/team-auto.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `--team auto`: discover a team config, spawn a local hooteams server as a\n * child process, and hand back its URL so the rest of the pipeline behaves\n * exactly as if `--team http://localhost:<port>` had been passed.\n *\n * hooteams is intentionally not bundled — the launcher is resolved from PATH\n * (`hooteams`, falling back to `bunx hooteams`) and missing pieces fail with\n * a clear, actionable error. The child is reaped on hoocode exit, clean or\n * signalled, via a process \"exit\" hook (the interactive shutdown path calls\n * process.exit directly, so an async cleanup would never run).\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { accessSync, constants, existsSync } from \"node:fs\";\nimport { createServer } from \"node:net\";\nimport path from \"node:path\";\n\n/** Config locations probed at each directory level, in priority order. */\nconst TEAM_CONFIG_CANDIDATES = [path.join(\".agents\", \"teams\", \"default.json\"), \"hooteams.config.json\"];\n\n/**\n * Walk up from startDir to the filesystem root, returning the first config\n * found. Both candidates are probed per level (.agents/teams/default.json\n * wins over hooteams.config.json in the same directory).\n */\nexport function findTeamConfig(startDir: string): string | undefined {\n\tlet dir = path.resolve(startDir);\n\twhile (true) {\n\t\tfor (const candidate of TEAM_CONFIG_CANDIDATES) {\n\t\t\tconst candidatePath = path.join(dir, candidate);\n\t\t\tif (existsSync(candidatePath)) return candidatePath;\n\t\t}\n\t\tconst parent = path.dirname(dir);\n\t\tif (parent === dir) return undefined;\n\t\tdir = parent;\n\t}\n}\n\n/** Ask the OS for a free port by binding port 0 and reading the assignment. */\nexport function findFreePort(): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = createServer();\n\t\tserver.once(\"error\", reject);\n\t\tserver.listen(0, \"127.0.0.1\", () => {\n\t\t\tconst address = server.address();\n\t\t\tif (address === null || typeof address === \"string\") {\n\t\t\t\tserver.close(() => reject(new Error(\"could not determine a free port\")));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { port } = address;\n\t\t\tserver.close(() => resolve(port));\n\t\t});\n\t});\n}\n\nfunction isExecutableOnPath(name: string, env: NodeJS.ProcessEnv): boolean {\n\tconst pathVar = env.PATH ?? \"\";\n\tconst extensions = process.platform === \"win32\" ? (env.PATHEXT ?? \".EXE;.CMD;.BAT;.COM\").split(\";\") : [\"\"];\n\tfor (const dir of pathVar.split(path.delimiter)) {\n\t\tif (!dir) continue;\n\t\tfor (const extension of extensions) {\n\t\t\ttry {\n\t\t\t\taccessSync(path.join(dir, name + extension.toLowerCase()), constants.X_OK);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\t// keep probing\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\n/** How to launch hooteams: directly from PATH, or through bunx. */\nexport function resolveHooteamsLauncher(\n\tenv: NodeJS.ProcessEnv = process.env,\n): { command: string; prefixArgs: string[] } | undefined {\n\tif (isExecutableOnPath(\"hooteams\", env)) return { command: \"hooteams\", prefixArgs: [] };\n\tif (isExecutableOnPath(\"bunx\", env)) return { command: \"bunx\", prefixArgs: [\"hooteams\"] };\n\treturn undefined;\n}\n\nexport interface AutoTeam {\n\t/** Base URL of the spawned hooteams server. */\n\turl: string;\n\t/** Graceful shutdown: POST /stop, then kill the child's process group. */\n\tstop(): Promise<void>;\n}\n\nexport interface AutoTeamOptions {\n\t/** Startup progress sink (pre-TUI, so console is fine). */\n\tlog?: (message: string) => void;\n\t/** How long to wait for GET /health (default 15s). */\n\thealthTimeoutMs?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nfunction killChild(child: ChildProcess): void {\n\tif (child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return;\n\ttry {\n\t\t// POSIX: the child leads its own process group (detached), so a negative\n\t\t// pid reaches hooteams even when launched through a bunx wrapper.\n\t\tif (process.platform !== \"win32\") process.kill(-child.pid, \"SIGTERM\");\n\t\telse child.kill();\n\t} catch {\n\t\t// Already gone.\n\t}\n}\n\nasync function waitForHealth(url: string, child: ChildProcess, timeoutMs: number): Promise<void> {\n\tconst deadline = Date.now() + timeoutMs;\n\twhile (Date.now() < deadline) {\n\t\tif (child.exitCode !== null || child.signalCode !== null) {\n\t\t\tthrow new Error(`--team auto: hooteams exited (code ${child.exitCode ?? \"signal\"}) before becoming healthy`);\n\t\t}\n\t\ttry {\n\t\t\tconst response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(1000) });\n\t\t\tif (response.ok) {\n\t\t\t\tconst body = (await response.json()) as { ok?: boolean };\n\t\t\t\tif (body.ok === true) return;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not up yet; keep polling.\n\t\t}\n\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\n\t}\n\tthrow new Error(`--team auto: hooteams did not report healthy at ${url}/health within ${timeoutMs}ms`);\n}\n\n/**\n * Resolve the config, spawn hooteams on a free port, and wait for /health.\n * Throws (with a message ready for the terminal) when no config is found, no\n * launcher resolves, or the server never becomes healthy.\n */\nexport async function startAutoTeam(cwd: string, options: AutoTeamOptions = {}): Promise<AutoTeam> {\n\tconst env = options.env ?? process.env;\n\tconst config = findTeamConfig(cwd);\n\tif (!config) {\n\t\tthrow new Error(\n\t\t\t`--team auto: no team config found. Looked for ${TEAM_CONFIG_CANDIDATES.join(\" or \")} in ${cwd} and every parent directory.`,\n\t\t);\n\t}\n\tconst launcher = resolveHooteamsLauncher(env);\n\tif (!launcher) {\n\t\tthrow new Error(\n\t\t\t\"--team auto: hooteams is not on PATH and bunx is unavailable. Install hooteams (or bun) or pass --team <url> to use a running server.\",\n\t\t);\n\t}\n\n\tconst port = await findFreePort();\n\tconst url = `http://localhost:${port}`;\n\toptions.log?.(`Starting hooteams (config ${config}) on port ${port}…`);\n\n\tconst child = spawn(\n\t\tlauncher.command,\n\t\t[...launcher.prefixArgs, \"start\", \"--config\", config, \"--port\", String(port)],\n\t\t{\n\t\t\tstdio: \"ignore\",\n\t\t\tenv,\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t},\n\t);\n\tchild.unref();\n\tconst reapOnExit = () => killChild(child);\n\tprocess.on(\"exit\", reapOnExit);\n\n\ttry {\n\t\tawait waitForHealth(url, child, options.healthTimeoutMs ?? 15000);\n\t} catch (error) {\n\t\tprocess.off(\"exit\", reapOnExit);\n\t\tkillChild(child);\n\t\tthrow error;\n\t}\n\n\treturn {\n\t\turl,\n\t\tasync stop() {\n\t\t\tprocess.off(\"exit\", reapOnExit);\n\t\t\ttry {\n\t\t\t\tawait fetch(`${url}/stop`, { method: \"POST\", signal: AbortSignal.timeout(2000) });\n\t\t\t} catch {\n\t\t\t\t// Graceful stop is best-effort; the kill below is the guarantee.\n\t\t\t}\n\t\t\tkillChild(child);\n\t\t},\n\t};\n}\n"]}
|
|
@@ -1,17 +1,4 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
export interface TokenBudgetConfig {
|
|
3
|
-
/** Budget limit in tokens. */
|
|
4
|
-
limit: number;
|
|
5
|
-
}
|
|
6
|
-
export interface TokenBudgetState {
|
|
7
|
-
task_id: string;
|
|
8
|
-
agent_type: string;
|
|
9
|
-
budget: number;
|
|
10
|
-
used: number;
|
|
11
|
-
warned: boolean;
|
|
12
|
-
exceeded: boolean;
|
|
13
|
-
last_updated: number;
|
|
14
|
-
}
|
|
15
2
|
/** Get the default budget for an agent type. */
|
|
16
3
|
export declare function getDefaultBudget(agent_type: string): number;
|
|
17
4
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-budget.d.ts","sourceRoot":"","sources":["../../src/core/token-budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"token-budget.d.ts","sourceRoot":"","sources":["../../src/core/token-budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA6B3C,gDAAgD;AAChD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,WAAY,SAAQ,YAAY;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAM;IAE1B,+BAA+B;IAC/B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,kCAAkC;IAClC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE3C,YAAY,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAO,EAQ9F;IAED;;;;;OAKG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAK9B;IAED;;;;;OAKG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAYjC;IAED,sEAAsE;IACtE,KAAK,IAAI,IAAI,CAKZ;IAED,sCAAsC;IACtC,OAAO,IAAI,MAAM,CAEhB;IAED,qCAAqC;IACrC,QAAQ,IAAI,MAAM,CAEjB;IAED,qDAAqD;IACrD,QAAQ,IAAI,OAAO,CAElB;IAED,4CAA4C;IAC5C,UAAU,IAAI,OAAO,CAEpB;IAED,4CAA4C;IAC5C,OAAO,IAAI,IAAI,CAkBd;IAED,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;CAgDjB","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getDispatchTaskDir } from \"../config.js\";\n\ninterface TokenBudgetState {\n\ttask_id: string;\n\tagent_type: string;\n\tbudget: number;\n\tused: number;\n\twarned: boolean;\n\texceeded: boolean;\n\tlast_updated: number;\n}\n\n/**\n * Default token budgets per agent type (in tokens). Raised from the previous\n * 8k–16k range, which was too low and hard-stopped subagents mid-task. Keyed by\n * the built-in agents; custom agents fall back to the default below.\n * - explore: 35 000 (read-only scanning, tracing deps)\n * - plan: 35 000 (read-only research to back a plan)\n * - general-purpose: 60 000 (reads, plans, writes, runs, iterates — heaviest)\n */\nconst DEFAULT_BUDGETS: Record<string, number> = {\n\texplore: 35000,\n\tplan: 35000,\n\t\"general-purpose\": 60000,\n};\n\n/** Get the default budget for an agent type. */\nexport function getDefaultBudget(agent_type: string): number {\n\treturn DEFAULT_BUDGETS[agent_type] ?? 35000;\n}\n\n/**\n * Tracks cumulative token usage for a single subagent task by parsing\n * newline-delimited JSON events from the subagent's stdout stream.\n *\n * Advisory only: this never stops or penalizes a subagent. The guaranteed hard\n * stop is the per-subagent turn cap (--max-turns); these events exist purely for\n * telemetry and for callers that want to surface usage.\n *\n * Emits:\n * - \"budget_warning\" when 80% of the budget is consumed\n * - \"budget_exceeded\" when 100% of the budget is consumed\n */\nexport class TokenBudget extends EventEmitter {\n\tprivate readonly task_id: string;\n\tprivate readonly agent_type: string;\n\tprivate readonly limit: number;\n\tprivate readonly cwd: string;\n\n\tprivate used = 0;\n\tprivate warned = false;\n\tprivate exceeded = false;\n\tprivate stdoutBuffer = \"\";\n\n\t/** Warning threshold (80%). */\n\tprivate readonly warningThreshold: number;\n\t/** Hard-stop threshold (100%). */\n\tprivate readonly exceededThreshold: number;\n\n\tconstructor(task_id: string, agent_type: string, options: { limit?: number; cwd?: string } = {}) {\n\t\tsuper();\n\t\tthis.task_id = task_id;\n\t\tthis.agent_type = agent_type;\n\t\tthis.limit = options.limit ?? getDefaultBudget(agent_type);\n\t\tthis.cwd = options.cwd ?? process.cwd();\n\t\tthis.warningThreshold = Math.floor(this.limit * 0.8);\n\t\tthis.exceededThreshold = this.limit;\n\t}\n\n\t/**\n\t * Process one complete JSONL line from the subagent's stdout. Use this when\n\t * an upstream reader already handles framing (UTF-8-safe reassembly of chunks\n\t * split mid-line or mid-character) — the pool feeds this from its single\n\t * attachJsonlLineReader so the stream is parsed exactly once.\n\t */\n\tprocessLine(line: string): void {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed) {\n\t\t\tthis.parseLine(trimmed);\n\t\t}\n\t}\n\n\t/**\n\t * Process a raw chunk of stdout data from the subagent, buffering until\n\t * newlines. Prefer processLine() when a UTF-8-safe line reader is available:\n\t * chunk boundaries here can split multi-byte characters, which corrupts the\n\t * line they fall in.\n\t */\n\tprocessStdout(chunk: string): void {\n\t\tthis.stdoutBuffer += chunk;\n\n\t\twhile (true) {\n\t\t\tconst lineEnd = this.stdoutBuffer.indexOf(\"\\n\");\n\t\t\tif (lineEnd === -1) break;\n\t\t\tconst line = this.stdoutBuffer.slice(0, lineEnd).trimEnd();\n\t\t\tthis.stdoutBuffer = this.stdoutBuffer.slice(lineEnd + 1);\n\t\t\tif (line) {\n\t\t\t\tthis.parseLine(line);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Flush any remaining buffered stdout. Call when the stream ends. */\n\tflush(): void {\n\t\tif (this.stdoutBuffer.trim()) {\n\t\t\tthis.parseLine(this.stdoutBuffer.trim());\n\t\t\tthis.stdoutBuffer = \"\";\n\t\t}\n\t}\n\n\t/** Current cumulative token usage. */\n\tgetUsed(): number {\n\t\treturn this.used;\n\t}\n\n\t/** Configured token budget limit. */\n\tgetLimit(): number {\n\t\treturn this.limit;\n\t}\n\n\t/** Whether the budget warning has been triggered. */\n\tisWarned(): boolean {\n\t\treturn this.warned;\n\t}\n\n\t/** Whether the budget has been exceeded. */\n\tisExceeded(): boolean {\n\t\treturn this.exceeded;\n\t}\n\n\t/** Persist current budget state to disk. */\n\tpersist(): void {\n\t\tconst state: TokenBudgetState = {\n\t\t\ttask_id: this.task_id,\n\t\t\tagent_type: this.agent_type,\n\t\t\tbudget: this.limit,\n\t\t\tused: this.used,\n\t\t\twarned: this.warned,\n\t\t\texceeded: this.exceeded,\n\t\t\tlast_updated: Date.now(),\n\t\t};\n\n\t\tconst path = this.budgetPath();\n\t\ttry {\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, JSON.stringify(state, null, 2));\n\t\t} catch {\n\t\t\t// Persistence is best-effort; silently ignore write failures\n\t\t}\n\t}\n\n\tprivate budgetPath(): string {\n\t\treturn join(getDispatchTaskDir(this.cwd, this.task_id), \"budget.json\");\n\t}\n\n\tprivate parseLine(line: string): void {\n\t\tlet event: unknown;\n\t\ttry {\n\t\t\tevent = JSON.parse(line);\n\t\t} catch {\n\t\t\treturn; // Not valid JSON, ignore\n\t\t}\n\n\t\tif (!event || typeof event !== \"object\") return;\n\n\t\t// Look for message_end events with assistant messages that have usage\n\t\tconst typedEvent = event as Record<string, unknown>;\n\t\tif (typedEvent.type !== \"message_end\") return;\n\n\t\tconst message = typedEvent.message as Record<string, unknown> | undefined;\n\t\tif (!message) return;\n\t\tif (message.role !== \"assistant\") return;\n\n\t\tconst usage = message.usage as Record<string, unknown> | undefined;\n\t\tif (!usage) return;\n\n\t\tconst totalTokens = typeof usage.totalTokens === \"number\" ? usage.totalTokens : undefined;\n\t\tif (totalTokens === undefined || totalTokens <= 0) return;\n\n\t\tthis.used += totalTokens;\n\n\t\t// Check thresholds\n\t\tif (!this.warned && this.used >= this.warningThreshold) {\n\t\t\tthis.warned = true;\n\t\t\tthis.emit(\"budget_warning\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tmessage: \"You are near token limit. Summarize and write result.json now.\",\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tif (!this.exceeded && this.used >= this.exceededThreshold) {\n\t\t\tthis.exceeded = true;\n\t\t\tthis.emit(\"budget_exceeded\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tthis.persist();\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-budget.js","sourceRoot":"","sources":["../../src/core/token-budget.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getDispatchTaskDir } from \"../config.js\";\n\nexport interface TokenBudgetConfig {\n\t/** Budget limit in tokens. */\n\tlimit: number;\n}\n\nexport interface TokenBudgetState {\n\ttask_id: string;\n\tagent_type: string;\n\tbudget: number;\n\tused: number;\n\twarned: boolean;\n\texceeded: boolean;\n\tlast_updated: number;\n}\n\n/**\n * Default token budgets per agent type (in tokens). Raised from the previous\n * 8k–16k range, which was too low and hard-stopped subagents mid-task. Keyed by\n * the built-in agents; custom agents fall back to the default below.\n * - explore: 35 000 (read-only scanning, tracing deps)\n * - plan: 35 000 (read-only research to back a plan)\n * - general-purpose: 60 000 (reads, plans, writes, runs, iterates — heaviest)\n */\nconst DEFAULT_BUDGETS: Record<string, number> = {\n\texplore: 35000,\n\tplan: 35000,\n\t\"general-purpose\": 60000,\n};\n\n/** Get the default budget for an agent type. */\nexport function getDefaultBudget(agent_type: string): number {\n\treturn DEFAULT_BUDGETS[agent_type] ?? 35000;\n}\n\n/**\n * Tracks cumulative token usage for a single subagent task by parsing\n * newline-delimited JSON events from the subagent's stdout stream.\n *\n * Advisory only: this never stops or penalizes a subagent. The guaranteed hard\n * stop is the per-subagent turn cap (--max-turns); these events exist purely for\n * telemetry and for callers that want to surface usage.\n *\n * Emits:\n * - \"budget_warning\" when 80% of the budget is consumed\n * - \"budget_exceeded\" when 100% of the budget is consumed\n */\nexport class TokenBudget extends EventEmitter {\n\tprivate readonly task_id: string;\n\tprivate readonly agent_type: string;\n\tprivate readonly limit: number;\n\tprivate readonly cwd: string;\n\n\tprivate used = 0;\n\tprivate warned = false;\n\tprivate exceeded = false;\n\tprivate stdoutBuffer = \"\";\n\n\t/** Warning threshold (80%). */\n\tprivate readonly warningThreshold: number;\n\t/** Hard-stop threshold (100%). */\n\tprivate readonly exceededThreshold: number;\n\n\tconstructor(task_id: string, agent_type: string, options: { limit?: number; cwd?: string } = {}) {\n\t\tsuper();\n\t\tthis.task_id = task_id;\n\t\tthis.agent_type = agent_type;\n\t\tthis.limit = options.limit ?? getDefaultBudget(agent_type);\n\t\tthis.cwd = options.cwd ?? process.cwd();\n\t\tthis.warningThreshold = Math.floor(this.limit * 0.8);\n\t\tthis.exceededThreshold = this.limit;\n\t}\n\n\t/**\n\t * Process one complete JSONL line from the subagent's stdout. Use this when\n\t * an upstream reader already handles framing (UTF-8-safe reassembly of chunks\n\t * split mid-line or mid-character) — the pool feeds this from its single\n\t * attachJsonlLineReader so the stream is parsed exactly once.\n\t */\n\tprocessLine(line: string): void {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed) {\n\t\t\tthis.parseLine(trimmed);\n\t\t}\n\t}\n\n\t/**\n\t * Process a raw chunk of stdout data from the subagent, buffering until\n\t * newlines. Prefer processLine() when a UTF-8-safe line reader is available:\n\t * chunk boundaries here can split multi-byte characters, which corrupts the\n\t * line they fall in.\n\t */\n\tprocessStdout(chunk: string): void {\n\t\tthis.stdoutBuffer += chunk;\n\n\t\twhile (true) {\n\t\t\tconst lineEnd = this.stdoutBuffer.indexOf(\"\\n\");\n\t\t\tif (lineEnd === -1) break;\n\t\t\tconst line = this.stdoutBuffer.slice(0, lineEnd).trimEnd();\n\t\t\tthis.stdoutBuffer = this.stdoutBuffer.slice(lineEnd + 1);\n\t\t\tif (line) {\n\t\t\t\tthis.parseLine(line);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Flush any remaining buffered stdout. Call when the stream ends. */\n\tflush(): void {\n\t\tif (this.stdoutBuffer.trim()) {\n\t\t\tthis.parseLine(this.stdoutBuffer.trim());\n\t\t\tthis.stdoutBuffer = \"\";\n\t\t}\n\t}\n\n\t/** Current cumulative token usage. */\n\tgetUsed(): number {\n\t\treturn this.used;\n\t}\n\n\t/** Configured token budget limit. */\n\tgetLimit(): number {\n\t\treturn this.limit;\n\t}\n\n\t/** Whether the budget warning has been triggered. */\n\tisWarned(): boolean {\n\t\treturn this.warned;\n\t}\n\n\t/** Whether the budget has been exceeded. */\n\tisExceeded(): boolean {\n\t\treturn this.exceeded;\n\t}\n\n\t/** Persist current budget state to disk. */\n\tpersist(): void {\n\t\tconst state: TokenBudgetState = {\n\t\t\ttask_id: this.task_id,\n\t\t\tagent_type: this.agent_type,\n\t\t\tbudget: this.limit,\n\t\t\tused: this.used,\n\t\t\twarned: this.warned,\n\t\t\texceeded: this.exceeded,\n\t\t\tlast_updated: Date.now(),\n\t\t};\n\n\t\tconst path = this.budgetPath();\n\t\ttry {\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, JSON.stringify(state, null, 2));\n\t\t} catch {\n\t\t\t// Persistence is best-effort; silently ignore write failures\n\t\t}\n\t}\n\n\tprivate budgetPath(): string {\n\t\treturn join(getDispatchTaskDir(this.cwd, this.task_id), \"budget.json\");\n\t}\n\n\tprivate parseLine(line: string): void {\n\t\tlet event: unknown;\n\t\ttry {\n\t\t\tevent = JSON.parse(line);\n\t\t} catch {\n\t\t\treturn; // Not valid JSON, ignore\n\t\t}\n\n\t\tif (!event || typeof event !== \"object\") return;\n\n\t\t// Look for message_end events with assistant messages that have usage\n\t\tconst typedEvent = event as Record<string, unknown>;\n\t\tif (typedEvent.type !== \"message_end\") return;\n\n\t\tconst message = typedEvent.message as Record<string, unknown> | undefined;\n\t\tif (!message) return;\n\t\tif (message.role !== \"assistant\") return;\n\n\t\tconst usage = message.usage as Record<string, unknown> | undefined;\n\t\tif (!usage) return;\n\n\t\tconst totalTokens = typeof usage.totalTokens === \"number\" ? usage.totalTokens : undefined;\n\t\tif (totalTokens === undefined || totalTokens <= 0) return;\n\n\t\tthis.used += totalTokens;\n\n\t\t// Check thresholds\n\t\tif (!this.warned && this.used >= this.warningThreshold) {\n\t\t\tthis.warned = true;\n\t\t\tthis.emit(\"budget_warning\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tmessage: \"You are near token limit. Summarize and write result.json now.\",\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tif (!this.exceeded && this.used >= this.exceededThreshold) {\n\t\t\tthis.exceeded = true;\n\t\t\tthis.emit(\"budget_exceeded\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tthis.persist();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"token-budget.js","sourceRoot":"","sources":["../../src/core/token-budget.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getDispatchTaskDir } from \"../config.js\";\n\ninterface TokenBudgetState {\n\ttask_id: string;\n\tagent_type: string;\n\tbudget: number;\n\tused: number;\n\twarned: boolean;\n\texceeded: boolean;\n\tlast_updated: number;\n}\n\n/**\n * Default token budgets per agent type (in tokens). Raised from the previous\n * 8k–16k range, which was too low and hard-stopped subagents mid-task. Keyed by\n * the built-in agents; custom agents fall back to the default below.\n * - explore: 35 000 (read-only scanning, tracing deps)\n * - plan: 35 000 (read-only research to back a plan)\n * - general-purpose: 60 000 (reads, plans, writes, runs, iterates — heaviest)\n */\nconst DEFAULT_BUDGETS: Record<string, number> = {\n\texplore: 35000,\n\tplan: 35000,\n\t\"general-purpose\": 60000,\n};\n\n/** Get the default budget for an agent type. */\nexport function getDefaultBudget(agent_type: string): number {\n\treturn DEFAULT_BUDGETS[agent_type] ?? 35000;\n}\n\n/**\n * Tracks cumulative token usage for a single subagent task by parsing\n * newline-delimited JSON events from the subagent's stdout stream.\n *\n * Advisory only: this never stops or penalizes a subagent. The guaranteed hard\n * stop is the per-subagent turn cap (--max-turns); these events exist purely for\n * telemetry and for callers that want to surface usage.\n *\n * Emits:\n * - \"budget_warning\" when 80% of the budget is consumed\n * - \"budget_exceeded\" when 100% of the budget is consumed\n */\nexport class TokenBudget extends EventEmitter {\n\tprivate readonly task_id: string;\n\tprivate readonly agent_type: string;\n\tprivate readonly limit: number;\n\tprivate readonly cwd: string;\n\n\tprivate used = 0;\n\tprivate warned = false;\n\tprivate exceeded = false;\n\tprivate stdoutBuffer = \"\";\n\n\t/** Warning threshold (80%). */\n\tprivate readonly warningThreshold: number;\n\t/** Hard-stop threshold (100%). */\n\tprivate readonly exceededThreshold: number;\n\n\tconstructor(task_id: string, agent_type: string, options: { limit?: number; cwd?: string } = {}) {\n\t\tsuper();\n\t\tthis.task_id = task_id;\n\t\tthis.agent_type = agent_type;\n\t\tthis.limit = options.limit ?? getDefaultBudget(agent_type);\n\t\tthis.cwd = options.cwd ?? process.cwd();\n\t\tthis.warningThreshold = Math.floor(this.limit * 0.8);\n\t\tthis.exceededThreshold = this.limit;\n\t}\n\n\t/**\n\t * Process one complete JSONL line from the subagent's stdout. Use this when\n\t * an upstream reader already handles framing (UTF-8-safe reassembly of chunks\n\t * split mid-line or mid-character) — the pool feeds this from its single\n\t * attachJsonlLineReader so the stream is parsed exactly once.\n\t */\n\tprocessLine(line: string): void {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed) {\n\t\t\tthis.parseLine(trimmed);\n\t\t}\n\t}\n\n\t/**\n\t * Process a raw chunk of stdout data from the subagent, buffering until\n\t * newlines. Prefer processLine() when a UTF-8-safe line reader is available:\n\t * chunk boundaries here can split multi-byte characters, which corrupts the\n\t * line they fall in.\n\t */\n\tprocessStdout(chunk: string): void {\n\t\tthis.stdoutBuffer += chunk;\n\n\t\twhile (true) {\n\t\t\tconst lineEnd = this.stdoutBuffer.indexOf(\"\\n\");\n\t\t\tif (lineEnd === -1) break;\n\t\t\tconst line = this.stdoutBuffer.slice(0, lineEnd).trimEnd();\n\t\t\tthis.stdoutBuffer = this.stdoutBuffer.slice(lineEnd + 1);\n\t\t\tif (line) {\n\t\t\t\tthis.parseLine(line);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Flush any remaining buffered stdout. Call when the stream ends. */\n\tflush(): void {\n\t\tif (this.stdoutBuffer.trim()) {\n\t\t\tthis.parseLine(this.stdoutBuffer.trim());\n\t\t\tthis.stdoutBuffer = \"\";\n\t\t}\n\t}\n\n\t/** Current cumulative token usage. */\n\tgetUsed(): number {\n\t\treturn this.used;\n\t}\n\n\t/** Configured token budget limit. */\n\tgetLimit(): number {\n\t\treturn this.limit;\n\t}\n\n\t/** Whether the budget warning has been triggered. */\n\tisWarned(): boolean {\n\t\treturn this.warned;\n\t}\n\n\t/** Whether the budget has been exceeded. */\n\tisExceeded(): boolean {\n\t\treturn this.exceeded;\n\t}\n\n\t/** Persist current budget state to disk. */\n\tpersist(): void {\n\t\tconst state: TokenBudgetState = {\n\t\t\ttask_id: this.task_id,\n\t\t\tagent_type: this.agent_type,\n\t\t\tbudget: this.limit,\n\t\t\tused: this.used,\n\t\t\twarned: this.warned,\n\t\t\texceeded: this.exceeded,\n\t\t\tlast_updated: Date.now(),\n\t\t};\n\n\t\tconst path = this.budgetPath();\n\t\ttry {\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, JSON.stringify(state, null, 2));\n\t\t} catch {\n\t\t\t// Persistence is best-effort; silently ignore write failures\n\t\t}\n\t}\n\n\tprivate budgetPath(): string {\n\t\treturn join(getDispatchTaskDir(this.cwd, this.task_id), \"budget.json\");\n\t}\n\n\tprivate parseLine(line: string): void {\n\t\tlet event: unknown;\n\t\ttry {\n\t\t\tevent = JSON.parse(line);\n\t\t} catch {\n\t\t\treturn; // Not valid JSON, ignore\n\t\t}\n\n\t\tif (!event || typeof event !== \"object\") return;\n\n\t\t// Look for message_end events with assistant messages that have usage\n\t\tconst typedEvent = event as Record<string, unknown>;\n\t\tif (typedEvent.type !== \"message_end\") return;\n\n\t\tconst message = typedEvent.message as Record<string, unknown> | undefined;\n\t\tif (!message) return;\n\t\tif (message.role !== \"assistant\") return;\n\n\t\tconst usage = message.usage as Record<string, unknown> | undefined;\n\t\tif (!usage) return;\n\n\t\tconst totalTokens = typeof usage.totalTokens === \"number\" ? usage.totalTokens : undefined;\n\t\tif (totalTokens === undefined || totalTokens <= 0) return;\n\n\t\tthis.used += totalTokens;\n\n\t\t// Check thresholds\n\t\tif (!this.warned && this.used >= this.warningThreshold) {\n\t\t\tthis.warned = true;\n\t\t\tthis.emit(\"budget_warning\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tmessage: \"You are near token limit. Summarize and write result.json now.\",\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tif (!this.exceeded && this.used >= this.exceededThreshold) {\n\t\t\tthis.exceeded = true;\n\t\t\tthis.emit(\"budget_exceeded\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tthis.persist();\n\t}\n}\n"]}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* reused across rounds. See {@link browsertools-shared}.
|
|
9
9
|
*/
|
|
10
10
|
import type { AgentTool } from "@kolisachint/hoocode-agent-core";
|
|
11
|
-
import {
|
|
11
|
+
import { Type } from "typebox";
|
|
12
12
|
import { type ToolDefinition } from "../../extensions/types.js";
|
|
13
13
|
import { type BrowserRunDetails } from "./browser-run.js";
|
|
14
14
|
import { type BrowsertoolsToolOptions } from "./browsertools-shared.js";
|
|
@@ -16,7 +16,6 @@ declare const browserContinueSchema: Type.TObject<{
|
|
|
16
16
|
token: Type.TString;
|
|
17
17
|
response: Type.TRecord<"^.*$", Type.TUnknown>;
|
|
18
18
|
}>;
|
|
19
|
-
export type BrowserContinueInput = Static<typeof browserContinueSchema>;
|
|
20
19
|
export interface BrowserContinueToolOptions extends BrowsertoolsToolOptions {
|
|
21
20
|
}
|
|
22
21
|
export declare function createBrowserContinueToolDefinition(_cwd: string, options?: BrowserContinueToolOptions): ToolDefinition<typeof browserContinueSchema, BrowserRunDetails>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-continue.d.ts","sourceRoot":"","sources":["../../../../src/core/tools/browser/browser-continue.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,
|
|
1
|
+
{"version":3,"file":"browser-continue.d.ts","sourceRoot":"","sources":["../../../../src/core/tools/browser/browser-continue.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE5E,OAAO,EAAe,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAEN,KAAK,uBAAuB,EAI5B,MAAM,0BAA0B,CAAC;AAElC,QAAA,MAAM,qBAAqB;;;EAOzB,CAAC;AAIH,MAAM,WAAW,0BAA2B,SAAQ,uBAAuB;CAAG;AAE9E,wBAAgB,mCAAmC,CAGlD,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,0BAA0B,GAClC,cAAc,CAAC,OAAO,qBAAqB,EAAE,iBAAiB,CAAC,CAyDjE;AAED,wBAAgB,yBAAyB,CACxC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,GAClC,SAAS,CAAC,OAAO,qBAAqB,CAAC,CAEzC","sourcesContent":["/**\n * `browser_continue` tool: answer a `NeedsParent` suspension from `browser_run`.\n *\n * Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`\n * with the parent's decision (a `ParentResponse` object), and maps the next\n * outcome the same way `browser_run` does — completing, failing, or suspending\n * again with a fresh token. The live serve process (and its browser state) is\n * reused across rounds. See {@link browsertools-shared}.\n */\n\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../../extensions/types.js\";\nimport { wrapToolDefinition } from \"../tool-definition-wrapper.js\";\nimport { advanceFlow, type BrowserRunDetails } from \"./browser-run.js\";\nimport {\n\ttype BrowserClientConfig,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\n\nconst browserContinueSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_run NeedsParent result.\" }),\n\tresponse: Type.Record(Type.String(), Type.Unknown(), {\n\t\tdescription:\n\t\t\t\"The ParentResponse object answering the request, e.g. \" +\n\t\t\t'{ \"response\": \"state\", \"state\": \"logged_in\" } or { \"response\": \"verified\", \"passed\": true }.',\n\t}),\n});\n\ntype BrowserContinueInput = Static<typeof browserContinueSchema>;\n\nexport interface BrowserContinueToolOptions extends BrowsertoolsToolOptions {}\n\nexport function createBrowserContinueToolDefinition(\n\t// cwd is part of the factory signature for parity with other tools, but continue\n\t// reuses the serve process parked by browser_run, so it is not needed here.\n\t_cwd: string,\n\toptions?: BrowserContinueToolOptions,\n): ToolDefinition<typeof browserContinueSchema, BrowserRunDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_continue\",\n\t\tlabel: \"browser continue\",\n\t\tdescription:\n\t\t\t\"Continue a browser flow that suspended with a NeedsParent request. Pass the token from the \" +\n\t\t\t\"browser_run result and a ParentResponse object answering the request. The flow continues \" +\n\t\t\t\"deterministically and either completes, fails, or suspends again with a new token — in which \" +\n\t\t\t\"case read the new screenshot and call browser_continue again, looping until the outcome is \" +\n\t\t\t\"`complete`. Do not abandon the loop to read the page with webfetch. ParentResponse by request \" +\n\t\t\t'kind: decide_next_action -> { response: \"next_action\", action: <Action e.g. {action:\"click\", ' +\n\t\t\t'selector, fallbacks?}> }; classify_state -> { response: \"state\", state }; verify_visual -> ' +\n\t\t\t'{ response: \"verified\", passed }; extract_semantic -> { response: \"extracted\", fields }; ' +\n\t\t\t'reidentify_element -> { response: \"element\", selector }. Off by default; enabled with ' +\n\t\t\t\"--enable-browsertools.\",\n\t\tparameters: browserContinueSchema,\n\t\tasync execute(_toolCallId, params: BrowserContinueInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst session = takeSession(params.token);\n\t\t\tif (!session) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No paused browser flow for token \"${params.token}\" — it may have completed, expired (idle ` +\n\t\t\t\t\t\t`timeout), or never existed. Start a new flow with browser_run.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst { client } = session;\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_resume\", {\n\t\t\t\t\ttoken: params.token,\n\t\t\t\t\tresponse: params.response,\n\t\t\t\t});\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\t// Preserve the original browser config so the idle client can be reused\n\t\t\t\t// by a subsequent browser_run with the same headful/browserPath settings.\n\t\t\t\tconst browserConfig: BrowserClientConfig = {\n\t\t\t\t\theadful: session.headful,\n\t\t\t\t\tbrowserPath: session.browserPath,\n\t\t\t\t\tidleTimeoutMs: opts.idleTimeoutMs,\n\t\t\t\t};\n\t\t\t\t// Count this resume as one more NeedsParent round if it suspends again.\n\t\t\t\treturn await advanceFlow(client, outcome, session.rounds + 1, opts, browserConfig);\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserContinueTool(\n\tcwd: string,\n\toptions?: BrowserContinueToolOptions,\n): AgentTool<typeof browserContinueSchema> {\n\treturn wrapToolDefinition(createBrowserContinueToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-continue.js","sourceRoot":"","sources":["../../../../src/core/tools/browser/browser-continue.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `browser_continue` tool: answer a `NeedsParent` suspension from `browser_run`.\n *\n * Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`\n * with the parent's decision (a `ParentResponse` object), and maps the next\n * outcome the same way `browser_run` does — completing, failing, or suspending\n * again with a fresh token. The live serve process (and its browser state) is\n * reused across rounds. See {@link browsertools-shared}.\n */\n\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../../extensions/types.js\";\nimport { wrapToolDefinition } from \"../tool-definition-wrapper.js\";\nimport { advanceFlow, type BrowserRunDetails } from \"./browser-run.js\";\nimport {\n\ttype BrowserClientConfig,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\n\nconst browserContinueSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_run NeedsParent result.\" }),\n\tresponse: Type.Record(Type.String(), Type.Unknown(), {\n\t\tdescription:\n\t\t\t\"The ParentResponse object answering the request, e.g. \" +\n\t\t\t'{ \"response\": \"state\", \"state\": \"logged_in\" } or { \"response\": \"verified\", \"passed\": true }.',\n\t}),\n});\n\
|
|
1
|
+
{"version":3,"file":"browser-continue.js","sourceRoot":"","sources":["../../../../src/core/tools/browser/browser-continue.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `browser_continue` tool: answer a `NeedsParent` suspension from `browser_run`.\n *\n * Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`\n * with the parent's decision (a `ParentResponse` object), and maps the next\n * outcome the same way `browser_run` does — completing, failing, or suspending\n * again with a fresh token. The live serve process (and its browser state) is\n * reused across rounds. See {@link browsertools-shared}.\n */\n\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../../extensions/types.js\";\nimport { wrapToolDefinition } from \"../tool-definition-wrapper.js\";\nimport { advanceFlow, type BrowserRunDetails } from \"./browser-run.js\";\nimport {\n\ttype BrowserClientConfig,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\n\nconst browserContinueSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_run NeedsParent result.\" }),\n\tresponse: Type.Record(Type.String(), Type.Unknown(), {\n\t\tdescription:\n\t\t\t\"The ParentResponse object answering the request, e.g. \" +\n\t\t\t'{ \"response\": \"state\", \"state\": \"logged_in\" } or { \"response\": \"verified\", \"passed\": true }.',\n\t}),\n});\n\ntype BrowserContinueInput = Static<typeof browserContinueSchema>;\n\nexport interface BrowserContinueToolOptions extends BrowsertoolsToolOptions {}\n\nexport function createBrowserContinueToolDefinition(\n\t// cwd is part of the factory signature for parity with other tools, but continue\n\t// reuses the serve process parked by browser_run, so it is not needed here.\n\t_cwd: string,\n\toptions?: BrowserContinueToolOptions,\n): ToolDefinition<typeof browserContinueSchema, BrowserRunDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_continue\",\n\t\tlabel: \"browser continue\",\n\t\tdescription:\n\t\t\t\"Continue a browser flow that suspended with a NeedsParent request. Pass the token from the \" +\n\t\t\t\"browser_run result and a ParentResponse object answering the request. The flow continues \" +\n\t\t\t\"deterministically and either completes, fails, or suspends again with a new token — in which \" +\n\t\t\t\"case read the new screenshot and call browser_continue again, looping until the outcome is \" +\n\t\t\t\"`complete`. Do not abandon the loop to read the page with webfetch. ParentResponse by request \" +\n\t\t\t'kind: decide_next_action -> { response: \"next_action\", action: <Action e.g. {action:\"click\", ' +\n\t\t\t'selector, fallbacks?}> }; classify_state -> { response: \"state\", state }; verify_visual -> ' +\n\t\t\t'{ response: \"verified\", passed }; extract_semantic -> { response: \"extracted\", fields }; ' +\n\t\t\t'reidentify_element -> { response: \"element\", selector }. Off by default; enabled with ' +\n\t\t\t\"--enable-browsertools.\",\n\t\tparameters: browserContinueSchema,\n\t\tasync execute(_toolCallId, params: BrowserContinueInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst session = takeSession(params.token);\n\t\t\tif (!session) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No paused browser flow for token \"${params.token}\" — it may have completed, expired (idle ` +\n\t\t\t\t\t\t`timeout), or never existed. Start a new flow with browser_run.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst { client } = session;\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_resume\", {\n\t\t\t\t\ttoken: params.token,\n\t\t\t\t\tresponse: params.response,\n\t\t\t\t});\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\t// Preserve the original browser config so the idle client can be reused\n\t\t\t\t// by a subsequent browser_run with the same headful/browserPath settings.\n\t\t\t\tconst browserConfig: BrowserClientConfig = {\n\t\t\t\t\theadful: session.headful,\n\t\t\t\t\tbrowserPath: session.browserPath,\n\t\t\t\t\tidleTimeoutMs: opts.idleTimeoutMs,\n\t\t\t\t};\n\t\t\t\t// Count this resume as one more NeedsParent round if it suspends again.\n\t\t\t\treturn await advanceFlow(client, outcome, session.rounds + 1, opts, browserConfig);\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserContinueTool(\n\tcwd: string,\n\toptions?: BrowserContinueToolOptions,\n): AgentTool<typeof browserContinueSchema> {\n\treturn wrapToolDefinition(createBrowserContinueToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* with the companion `browser_continue` tool. See {@link browsertools-shared}.
|
|
11
11
|
*/
|
|
12
12
|
import type { AgentTool } from "@kolisachint/hoocode-agent-core";
|
|
13
|
-
import {
|
|
13
|
+
import { Type } from "typebox";
|
|
14
14
|
import type { AgentToolResult } from "../../extensions/types.js";
|
|
15
15
|
import { type ToolDefinition } from "../../extensions/types.js";
|
|
16
16
|
import { type BrowserClientConfig, BrowsertoolsServeClient, type BrowsertoolsToolOptions, type FlowOutcome, type ParentRequest, type ResumeToken, resolveBrowsertoolsOptions } from "./browsertools-shared.js";
|
|
@@ -22,7 +22,6 @@ declare const browserRunSchema: Type.TObject<{
|
|
|
22
22
|
live_view: Type.TOptional<Type.TBoolean>;
|
|
23
23
|
headful: Type.TOptional<Type.TBoolean>;
|
|
24
24
|
}>;
|
|
25
|
-
export type BrowserRunInput = Static<typeof browserRunSchema>;
|
|
26
25
|
/** Structured details surfaced alongside the model-facing content. */
|
|
27
26
|
export interface BrowserRunDetails {
|
|
28
27
|
status: "complete" | "needs_parent";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-run.d.ts","sourceRoot":"","sources":["../../../../src/core/tools/browser/browser-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE5E,OAAO,EACN,KAAK,mBAAmB,EACxB,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAEhB,KAAK,aAAa,EAGlB,KAAK,WAAW,EAEhB,0BAA0B,EAE1B,MAAM,0BAA0B,CAAC;AAElC,QAAA,MAAM,gBAAgB;;;;;;;EAmDpB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE9D,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,UAAU,GAAG,cAAc,CAAC;IACpC,qEAAqE;IACrE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,yEAAyE;IACzE,WAAW,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACvC,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;CAAG;AAkMzE;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC,EACnD,aAAa,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC,CA2D7C;AAED,wBAAgB,8BAA8B,CAC7C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,qBAAqB,GAC7B,cAAc,CAAC,OAAO,gBAAgB,EAAE,iBAAiB,CAAC,CAiG5D;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC,OAAO,gBAAgB,CAAC,CAErH","sourcesContent":["/**\n * `browser_run` tool: start a deterministic browsertools flow.\n *\n * Spawns a `browsertools serve` process and issues `flow_start`. If the flow\n * runs to completion deterministically it returns the evidence inline. If replay\n * hits a point only an LLM can resolve, the serve process suspends with\n * `Outcome::NeedsParent`: this tool fetches the suspension screenshot, parks the\n * live session under its `ResumeToken`, and returns the typed `ParentRequest`\n * (plus the screenshot as an image) to the agent. The agent reasons and answers\n * with the companion `browser_continue` tool. See {@link browsertools-shared}.\n */\n\nimport { exec } from \"node:child_process\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { ImageContent, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { type Static, Type } from \"typebox\";\nimport type { AgentToolResult } from \"../../extensions/types.js\";\nimport { defineTool, type ToolDefinition } from \"../../extensions/types.js\";\nimport { wrapToolDefinition } from \"../tool-definition-wrapper.js\";\nimport {\n\ttype BrowserClientConfig,\n\tBrowsertoolsServeClient,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\ttype GetResourceResult,\n\ttype ParentRequest,\n\tparkIdleClient,\n\tparkSession,\n\ttype ResumeToken,\n\tresolveBrowsertoolsBinary,\n\tresolveBrowsertoolsOptions,\n\ttakeIdleClient,\n} from \"./browsertools-shared.js\";\n\nconst browserRunSchema = Type.Object({\n\tflow_path: Type.Optional(\n\t\tType.String({ description: \"Path to the .flow.json file to execute. Provide this or `flow`.\" }),\n\t),\n\tflow: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription:\n\t\t\t\t\"Inline flow definition object (alternative to `flow_path`). Shape: \" +\n\t\t\t\t\"{ id: string, name: string, version: number, start_url: string, steps: Step[] }. \" +\n\t\t\t\t\"Each Step is { id: string, action: Action }. Action is a tagged object keyed by \" +\n\t\t\t\t\"`action`; the exact fields per variant (do NOT add extras like `goal` to the wrong \" +\n\t\t\t\t\"variant): \" +\n\t\t\t\t'{ action: \"navigate\", url: string } | ' +\n\t\t\t\t'{ action: \"click\", selector: string, fallbacks?: string[] } | ' +\n\t\t\t\t'{ action: \"fill\", selector: string, value_tpl: string } | ' +\n\t\t\t\t'{ action: \"select\", selector: string, value_tpl: string } | ' +\n\t\t\t\t'{ action: \"wait_settle\" } | ' +\n\t\t\t\t'{ action: \"checkpoint\", asserts: Invariant[] } | ' +\n\t\t\t\t'{ action: \"decide\", goal: string } | ' +\n\t\t\t\t'{ action: \"classify\" } | ' +\n\t\t\t\t'{ action: \"verify_visual\", expected_state: string } | ' +\n\t\t\t\t'{ action: \"extract_semantic\", fields: string[] }. ' +\n\t\t\t\t\"IMPORTANT: `extract_semantic.fields` MUST be an array of field-name strings \" +\n\t\t\t\t'(e.g. [\"person\", \"movie_count\"]), never an object/map, and `extract_semantic` takes ' +\n\t\t\t\t\"no `goal` (only `decide` does). Invariant (for checkpoint asserts) is one of: \" +\n\t\t\t\t'{ kind: \"element_present\", selector: string } | ' +\n\t\t\t\t'{ kind: \"text_present\", selector?: string, substr: string } | ' +\n\t\t\t\t'{ kind: \"url_matches\", pattern: string }.',\n\t\t}),\n\t),\n\tvars: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Variables interpolated into the flow ({{var}} placeholders).\",\n\t\t}),\n\t),\n\tstore: Type.Optional(Type.String({ description: \"Path to the evidence store directory for this run.\" })),\n\tlive_view: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Start a live viewer that streams the page and the agent's tool-call log over a local \" +\n\t\t\t\t\"WebSocket, and auto-open it in your default browser. Set HOOCODE_BROWSERTOOLS_NO_OPEN=1 to \" +\n\t\t\t\t\"print the URL without opening. Best for flows that suspend or run long.\",\n\t\t}),\n\t),\n\theadful: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Launch a real on-screen Chromium window instead of a headless browser. Requires a desktop \" +\n\t\t\t\t\"display; unlike live_view it does not show the tool-call log.\",\n\t\t}),\n\t),\n});\n\nexport type BrowserRunInput = Static<typeof browserRunSchema>;\n\n/** Structured details surfaced alongside the model-facing content. */\nexport interface BrowserRunDetails {\n\tstatus: \"complete\" | \"needs_parent\";\n\t/** Present when status is \"needs_parent\": resume with this token. */\n\ttoken?: ResumeToken;\n\t/** Present when status is \"needs_parent\": the kind of parent request. */\n\trequestKind?: ParentRequest[\"request\"];\n\t/** Present when status is \"complete\": the flow result/evidence. */\n\tresult?: unknown;\n}\n\nexport interface BrowserRunToolOptions extends BrowsertoolsToolOptions {}\n\n/** ParentResponse shape hint per ParentRequest kind (mirrors contract.rs). */\nfunction parentResponseHint(kind: ParentRequest[\"request\"]): string {\n\tswitch (kind) {\n\t\tcase \"classify_state\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"state\", \"state\": \"<your label>\" }';\n\t\tcase \"verify_visual\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"verified\", \"passed\": true | false }';\n\t\tcase \"extract_semantic\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"extracted\", \"fields\": { \"<field>\": \"<value>\", ... } }';\n\t\tcase \"decide_next_action\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"next_action\", \"action\": <action object> }';\n\t\tcase \"reidentify_element\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"element\", \"selector\": \"<css selector>\" }';\n\t\tdefault:\n\t\t\treturn \"Reply with browser_continue providing the appropriate ParentResponse object.\";\n\t}\n}\n\n/** A control fact from the browsertools observation (mirrors observe.rs InputFact). */\ninterface ObservationInput {\n\tkind?: string;\n\taccessible_name?: string;\n\tselector_hint?: string;\n}\n\n/** A landmark fact from the browsertools observation (mirrors observe.rs Landmark). */\ninterface ObservationLandmark {\n\trole?: string;\n\tname?: string;\n}\n\n/** The HTML-derived observation the engine attaches to decide/classify requests. */\ninterface Observation {\n\turl?: string;\n\ttitle?: string;\n\tinputs?: ObservationInput[];\n\tlandmarks?: ObservationLandmark[];\n\ttext_blocks?: string[];\n\thas_error_region?: boolean;\n}\n\n/** Truncate a string to `max` chars with an ellipsis, for compact rendering. */\nfunction clip(s: string, max = 80): string {\n\tconst t = s.trim();\n\treturn t.length > max ? `${t.slice(0, max - 1)}\\u2026` : t;\n}\n\n/** Render one observation control as a single compact line. */\nfunction renderControl(i: ObservationInput): string {\n\tconst sel = i.selector_hint && i.selector_hint !== \"button\" ? i.selector_hint : undefined;\n\tconst name = i.accessible_name ? `\"${clip(i.accessible_name, 60)}\"` : undefined;\n\tconst parts = [i.kind, name, sel].filter(Boolean);\n\treturn ` - ${parts.join(\" \")}`;\n}\n\n/**\n * Summarize a verbose `decide_next_action`/`classify_state` observation into a\n * compact, readable block instead of dumping the raw JSON (which on busy pages is\n * thousands of mostly-empty entries straight into the model context). The\n * screenshot already conveys the page; this is just the actionable text layer:\n * url/title, named controls, salient headings. Returns undefined when there is no\n * observation to summarize.\n */\nfunction summarizeObservation(observation: Observation | undefined): string | undefined {\n\tif (!observation || typeof observation !== \"object\") return undefined;\n\tconst lines: string[] = [];\n\tif (observation.title) lines.push(`page: ${clip(observation.title, 100)}`);\n\tif (observation.url) lines.push(`url: ${clip(observation.url, 120)}`);\n\tif (observation.has_error_region) lines.push(\"note: page has an error/alert region\");\n\n\tconst controls = (observation.inputs ?? []).filter((i) => i.kind !== \"_more\");\n\tif (controls.length) {\n\t\tlines.push(`controls (${controls.length}):`);\n\t\tfor (const c of controls.slice(0, 25)) lines.push(renderControl(c));\n\t\tif (controls.length > 25) lines.push(` - …+${controls.length - 25} more`);\n\t}\n\n\tconst text = (observation.text_blocks ?? []).filter((t) => t?.trim());\n\tif (text.length) {\n\t\tlines.push(\n\t\t\t`headings: ${text\n\t\t\t\t.slice(0, 12)\n\t\t\t\t.map((t) => clip(t, 50))\n\t\t\t\t.join(\" · \")}`,\n\t\t);\n\t}\n\treturn lines.length ? lines.join(\"\\n\") : undefined;\n}\n\n/**\n * Build the compact, model-facing text for a `NeedsParent` suspension. Renders\n * only the fields that matter per request kind (goal, expected_state, fields,\n * description) plus a summarized observation, rather than pretty-printing the\n * entire request — which is the dominant token sink for browser flows.\n */\nfunction formatParentRequest(request: ParentRequest): string {\n\tconst kind = request.request;\n\tconst lines: string[] = [`request: ${kind}`];\n\tconst r = request as ParentRequest & {\n\t\tgoal?: string;\n\t\texpected_state?: string;\n\t\tdescription?: string;\n\t\tfields?: string[];\n\t\tobservation?: Observation;\n\t};\n\tif (r.goal) lines.push(`goal: ${r.goal}`);\n\tif (r.expected_state) lines.push(`expected_state: ${r.expected_state}`);\n\tif (r.description) lines.push(`element: ${r.description}`);\n\tif (Array.isArray(r.fields) && r.fields.length) lines.push(`fields: ${r.fields.join(\", \")}`);\n\tconst summary = summarizeObservation(r.observation);\n\tif (summary) lines.push(summary);\n\treturn lines.join(\"\\n\");\n}\n\n/** Compact, model-facing reminder of the inline-flow action schema, appended to\n * an `invalid inline flow` error so the model can self-correct in one turn\n * instead of guessing field shapes across several rounds. */\nconst FLOW_SCHEMA_HINT =\n\t\"Inline flow shape: { id, name, version, start_url, steps: [{ id, action }] }. \" +\n\t\"Action variants (use EXACTLY these fields): \" +\n\t'{ action: \"navigate\", url } | { action: \"click\", selector, fallbacks? } | ' +\n\t'{ action: \"fill\", selector, value_tpl } | { action: \"select\", selector, value_tpl } | ' +\n\t'{ action: \"wait_settle\" } | { action: \"checkpoint\", asserts } | ' +\n\t'{ action: \"decide\", goal } | { action: \"classify\" } | ' +\n\t'{ action: \"verify_visual\", expected_state } | { action: \"extract_semantic\", fields }. ' +\n\t'`extract_semantic.fields` MUST be a string array like [\"person\",\"count\"] (not an object), ' +\n\t\"and it takes no `goal` — only `decide` does.\";\n\n/** If an error is a browsertools inline-flow validation failure, append the\n * schema hint so the model fixes the flow on the next call. Other errors pass\n * through unchanged. */\nfunction enrichFlowError(error: unknown): unknown {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (message.includes(\"invalid inline flow\") && !message.includes(\"Action variants\")) {\n\t\treturn new Error(`${message}\\n\\n${FLOW_SCHEMA_HINT}`);\n\t}\n\treturn error;\n}\n\n/** Best-effort: open a URL in the OS default browser. Never throws. Suppressed by\n * HOOCODE_BROWSERTOOLS_NO_OPEN (the URL is still surfaced to the agent). */\nfunction openInBrowser(url: string): boolean {\n\tconst suppress = process.env.HOOCODE_BROWSERTOOLS_NO_OPEN?.trim();\n\tif (suppress === \"1\" || suppress?.toLowerCase() === \"true\") return false;\n\tconst openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n\ttry {\n\t\texec(`${openCmd} \"${url}\"`);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Start the streamed live viewer and auto-open it. Best-effort: a failure here\n * must not abort the flow, so it degrades to returning undefined. Returns a\n * human-readable status line to prepend to the tool result, or undefined.\n *\n * When the serve client is reused across browser_run calls (the\n * reclaimed-idle path), the viewer is already running on the same port, so we\n * skip re-issuing `live_view_start` and skip auto-opening a second OS tab —\n * the user keeps the one tab they already have. */\nasync function startLiveView(client: BrowsertoolsServeClient): Promise<string | undefined> {\n\tif (client.liveViewUrl) {\n\t\treturn `Live view already open at: ${client.liveViewUrl}`;\n\t}\n\ttry {\n\t\tconst result = await client.request<{ url?: string; error?: string }>(\"live_view_start\", {});\n\t\tif (!result?.url) return undefined;\n\t\tclient.liveViewUrl = result.url;\n\t\tconst opened = openInBrowser(result.url);\n\t\treturn opened ? `Live view opened in your browser: ${result.url}` : `Live view available at: ${result.url}`;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Fetch the suspension screenshot for a ParentRequest as an ImageContent block.\n * Best-effort: a fetch failure degrades to no image rather than failing the flow. */\nasync function fetchScreenshot(\n\tclient: BrowsertoolsServeClient,\n\trequest: ParentRequest,\n): Promise<ImageContent | undefined> {\n\tif (!request.screenshot_ref) return undefined;\n\ttry {\n\t\tconst resource = await client.request<GetResourceResult>(\"get_resource\", { ref: request.screenshot_ref });\n\t\tif (!resource?.png_base64) return undefined;\n\t\treturn { type: \"image\", data: resource.png_base64, mimeType: resource.mime || \"image/png\" };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Map a `flow_start`/`flow_resume` outcome to a tool result, owning the live\n * client: dispose it on terminal outcomes, or park it under the new resume token\n * when the flow suspends again. `rounds` is the number of NeedsParent yields seen\n * so far for this flow (including the one being processed), used for the cap.\n */\nexport async function advanceFlow(\n\tclient: BrowsertoolsServeClient,\n\toutcome: FlowOutcome,\n\trounds: number,\n\topts: ReturnType<typeof resolveBrowsertoolsOptions>,\n\tbrowserConfig?: BrowserClientConfig,\n): Promise<AgentToolResult<BrowserRunDetails>> {\n\tif (outcome.outcome === \"complete\") {\n\t\tif (browserConfig) {\n\t\t\tparkIdleClient(client, browserConfig.headful, browserConfig.browserPath, browserConfig.idleTimeoutMs);\n\t\t} else {\n\t\t\tclient.dispose();\n\t\t}\n\t\tconst result = outcome.result;\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: `Flow complete.\\n${JSON.stringify(result ?? {}, null, 2)}` }],\n\t\t\tdetails: { status: \"complete\", result },\n\t\t};\n\t}\n\n\tif (outcome.outcome === \"failed\") {\n\t\tif (browserConfig) {\n\t\t\tparkIdleClient(client, browserConfig.headful, browserConfig.browserPath, browserConfig.idleTimeoutMs);\n\t\t} else {\n\t\t\tclient.dispose();\n\t\t}\n\t\tconst where = outcome.step_id ? ` at step \"${outcome.step_id}\"` : \"\";\n\t\tconst kind = outcome.kind ? ` (${outcome.kind})` : \"\";\n\t\tthrow new Error(`browsertools flow failed${where}: ${outcome.detail ?? \"unknown error\"}${kind}`);\n\t}\n\n\tif (outcome.outcome === \"needs_parent\") {\n\t\tif (rounds > opts.maxParentRounds) {\n\t\t\tclient.dispose();\n\t\t\tthrow new Error(\n\t\t\t\t`browsertools flow exceeded the maximum of ${opts.maxParentRounds} NeedsParent rounds; aborting to avoid a runaway loop`,\n\t\t\t);\n\t\t}\n\t\tconst { request, token } = outcome;\n\t\tconst image = await fetchScreenshot(client, request);\n\t\tparkSession(\n\t\t\ttoken,\n\t\t\tclient,\n\t\t\trounds,\n\t\t\tbrowserConfig?.idleTimeoutMs ?? opts.idleTimeoutMs,\n\t\t\tbrowserConfig?.headful ?? false,\n\t\t\tbrowserConfig?.browserPath,\n\t\t);\n\n\t\tconst text =\n\t\t\t`Flow suspended — parent decision required (NeedsParent).\\n` +\n\t\t\t`${formatParentRequest(request)}\\n` +\n\t\t\t`resume token: ${token}\\n` +\n\t\t\t`${parentResponseHint(request.request)}\\n` +\n\t\t\t(image ? \"A screenshot of the current page is attached.\" : \"(no screenshot available)\");\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (image) content.push(image);\n\t\treturn {\n\t\t\tcontent,\n\t\t\tdetails: { status: \"needs_parent\", token, requestKind: request.request },\n\t\t};\n\t}\n\n\tclient.dispose();\n\tthrow new Error(`browsertools returned an unrecognized flow outcome: ${JSON.stringify(outcome)}`);\n}\n\nexport function createBrowserRunToolDefinition(\n\tcwd: string,\n\toptions?: BrowserRunToolOptions,\n): ToolDefinition<typeof browserRunSchema, BrowserRunDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_run\",\n\t\tlabel: \"browser flow\",\n\t\tdescription:\n\t\t\t\"Start a deterministic browser flow (browsertools). Runs a saved .flow.json (or inline flow) \" +\n\t\t\t\"against a headless browser and returns the evidence on completion. If the flow needs an LLM \" +\n\t\t\t\"decision mid-replay (classify a page state, verify a visual, extract a value, decide the next \" +\n\t\t\t\"action, or re-identify a drifted element) it suspends and returns a typed request plus a \" +\n\t\t\t\"screenshot; answer it with the browser_continue tool using the returned token. Off by default; \" +\n\t\t\t\"enabled with --enable-browsertools.\\n\\n\" +\n\t\t\t\"AGENTIC LOOP (preferred for exploration): for any task where you must read or navigate based on \" +\n\t\t\t\"page content, build the flow from `decide`/`extract_semantic`/`classify`/`verify_visual` steps. \" +\n\t\t\t\"Each such step SUSPENDS and hands you a screenshot of the current page. Read the screenshot, then \" +\n\t\t\t\"call browser_continue with the next action, and keep looping until the outcome is `complete`. Do \" +\n\t\t\t\"NOT fall back to webfetch/curl to read page content you could read from the screenshot — that \" +\n\t\t\t\"bypasses the live session and breaks on auth-gated or JS-rendered pages. A flow ENDS as soon as \" +\n\t\t\t\"its last step runs, so chain several `decide` steps (interleaved with `wait_settle`) when you \" +\n\t\t\t\"need a multi-step journey (search -> open result -> scroll -> extract).\\n\\n\" +\n\t\t\t\"RESUME RESPONSE SHAPES (browser_continue `response` field): decide_next_action -> \" +\n\t\t\t'{ response: \"next_action\", action: <Action> }; classify_state -> { response: \"state\", state: \"<label>\" }; ' +\n\t\t\t'verify_visual -> { response: \"verified\", passed: true|false }; extract_semantic -> ' +\n\t\t\t'{ response: \"extracted\", fields: { <field>: <value> } }; reidentify_element -> ' +\n\t\t\t'{ response: \"element\", selector: \"<css>\" }.\\n\\n' +\n\t\t\t\"ACTION (for next_action) is the same shape as a flow step's action: { action: 'navigate', url }, \" +\n\t\t\t\"{ action: 'click', selector, fallbacks?: string[] }, { action: 'fill', selector, value_tpl }, \" +\n\t\t\t\"{ action: 'select', selector, value_tpl }, { action: 'wait_settle' }. Prefer stable CSS/id \" +\n\t\t\t\"selectors, and ALWAYS pass a `fallbacks` array of alternate selectors for click/fill, because \" +\n\t\t\t\"the primary selector often drifts (e.g. click '.suggestion-link' with fallbacks \" +\n\t\t\t\"['a.mw-searchSuggest-link', '#typeahead-suggestions a']).\\n\\n\" +\n\t\t\t\"VISIBILITY: pass headful:true to launch a real on-screen browser window the user can watch; \" +\n\t\t\t\"live_view:true additionally streams a mirror + tool-call log to a local URL (set live_view:false \" +\n\t\t\t\"to suppress the mirror when the instance defaults it on).\",\n\t\tpromptSnippet: \"Run a deterministic browser flow, pausing for LLM decisions when needed\",\n\t\tparameters: browserRunSchema,\n\t\tasync execute(_toolCallId, params: BrowserRunInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tif (!params.flow_path && !params.flow) {\n\t\t\t\tthrow new Error(\"browser_run requires either `flow_path` or `flow`\");\n\t\t\t}\n\n\t\t\tconst binaryPath = await resolveBrowsertoolsBinary(options);\n\t\t\tconst headful = params.headful ?? opts.headful;\n\t\t\t// Reuse the shared idle client if browser config matches, otherwise create a new\n\t\t\t// one. This keeps a single Chromium process and live-view port across calls.\n\t\t\tlet client = takeIdleClient(headful, opts.browserPath, opts.idleTimeoutMs);\n\t\t\tif (!client) {\n\t\t\t\tclient = new BrowsertoolsServeClient(binaryPath, {\n\t\t\t\t\tcwd,\n\t\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\t\tserveArgs: opts.serveArgs,\n\t\t\t\t\trequestTimeoutMs: opts.requestTimeoutMs,\n\t\t\t\t\theadful,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// If the call is aborted before we hand the client to the registry, make\n\t\t\t// sure the serve process is torn down.\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst startParams: Record<string, unknown> = {};\n\t\t\t\tif (params.flow_path) startParams.flow_path = params.flow_path;\n\t\t\t\tif (params.flow) startParams.flow = params.flow;\n\t\t\t\tif (params.vars) startParams.vars = params.vars;\n\t\t\t\tif (params.store) startParams.store = params.store;\n\n\t\t\t\t// Bring up the live viewer before the flow runs so the page render and\n\t\t\t\t// tool-call log are visible from the first step. The per-call param wins\n\t\t\t\t// over the instance default (--enable-browser-live-preview).\n\t\t\t\tconst liveViewEnabled = params.live_view ?? opts.liveView;\n\t\t\t\tconst liveViewStatus = liveViewEnabled ? await startLiveView(client) : undefined;\n\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_start\", startParams);\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\tconst browserConfig: BrowserClientConfig = {\n\t\t\t\t\theadful,\n\t\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\t\tidleTimeoutMs: opts.idleTimeoutMs,\n\t\t\t\t};\n\t\t\t\tconst result = await advanceFlow(client, outcome, 1, opts, browserConfig);\n\t\t\t\tif (liveViewStatus) {\n\t\t\t\t\tresult.content.unshift({ type: \"text\", text: liveViewStatus });\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow enrichFlowError(error);\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserRunTool(cwd: string, options?: BrowserRunToolOptions): AgentTool<typeof browserRunSchema> {\n\treturn wrapToolDefinition(createBrowserRunToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"browser-run.d.ts","sourceRoot":"","sources":["../../../../src/core/tools/browser/browser-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE5E,OAAO,EACN,KAAK,mBAAmB,EACxB,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAEhB,KAAK,aAAa,EAGlB,KAAK,WAAW,EAEhB,0BAA0B,EAE1B,MAAM,0BAA0B,CAAC;AAElC,QAAA,MAAM,gBAAgB;;;;;;;EAmDpB,CAAC;AAIH,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,UAAU,GAAG,cAAc,CAAC;IACpC,qEAAqE;IACrE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,yEAAyE;IACzE,WAAW,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACvC,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;CAAG;AAkMzE;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC,EACnD,aAAa,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC,CA2D7C;AAED,wBAAgB,8BAA8B,CAC7C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,qBAAqB,GAC7B,cAAc,CAAC,OAAO,gBAAgB,EAAE,iBAAiB,CAAC,CAiG5D;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC,OAAO,gBAAgB,CAAC,CAErH","sourcesContent":["/**\n * `browser_run` tool: start a deterministic browsertools flow.\n *\n * Spawns a `browsertools serve` process and issues `flow_start`. If the flow\n * runs to completion deterministically it returns the evidence inline. If replay\n * hits a point only an LLM can resolve, the serve process suspends with\n * `Outcome::NeedsParent`: this tool fetches the suspension screenshot, parks the\n * live session under its `ResumeToken`, and returns the typed `ParentRequest`\n * (plus the screenshot as an image) to the agent. The agent reasons and answers\n * with the companion `browser_continue` tool. See {@link browsertools-shared}.\n */\n\nimport { exec } from \"node:child_process\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { ImageContent, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { type Static, Type } from \"typebox\";\nimport type { AgentToolResult } from \"../../extensions/types.js\";\nimport { defineTool, type ToolDefinition } from \"../../extensions/types.js\";\nimport { wrapToolDefinition } from \"../tool-definition-wrapper.js\";\nimport {\n\ttype BrowserClientConfig,\n\tBrowsertoolsServeClient,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\ttype GetResourceResult,\n\ttype ParentRequest,\n\tparkIdleClient,\n\tparkSession,\n\ttype ResumeToken,\n\tresolveBrowsertoolsBinary,\n\tresolveBrowsertoolsOptions,\n\ttakeIdleClient,\n} from \"./browsertools-shared.js\";\n\nconst browserRunSchema = Type.Object({\n\tflow_path: Type.Optional(\n\t\tType.String({ description: \"Path to the .flow.json file to execute. Provide this or `flow`.\" }),\n\t),\n\tflow: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription:\n\t\t\t\t\"Inline flow definition object (alternative to `flow_path`). Shape: \" +\n\t\t\t\t\"{ id: string, name: string, version: number, start_url: string, steps: Step[] }. \" +\n\t\t\t\t\"Each Step is { id: string, action: Action }. Action is a tagged object keyed by \" +\n\t\t\t\t\"`action`; the exact fields per variant (do NOT add extras like `goal` to the wrong \" +\n\t\t\t\t\"variant): \" +\n\t\t\t\t'{ action: \"navigate\", url: string } | ' +\n\t\t\t\t'{ action: \"click\", selector: string, fallbacks?: string[] } | ' +\n\t\t\t\t'{ action: \"fill\", selector: string, value_tpl: string } | ' +\n\t\t\t\t'{ action: \"select\", selector: string, value_tpl: string } | ' +\n\t\t\t\t'{ action: \"wait_settle\" } | ' +\n\t\t\t\t'{ action: \"checkpoint\", asserts: Invariant[] } | ' +\n\t\t\t\t'{ action: \"decide\", goal: string } | ' +\n\t\t\t\t'{ action: \"classify\" } | ' +\n\t\t\t\t'{ action: \"verify_visual\", expected_state: string } | ' +\n\t\t\t\t'{ action: \"extract_semantic\", fields: string[] }. ' +\n\t\t\t\t\"IMPORTANT: `extract_semantic.fields` MUST be an array of field-name strings \" +\n\t\t\t\t'(e.g. [\"person\", \"movie_count\"]), never an object/map, and `extract_semantic` takes ' +\n\t\t\t\t\"no `goal` (only `decide` does). Invariant (for checkpoint asserts) is one of: \" +\n\t\t\t\t'{ kind: \"element_present\", selector: string } | ' +\n\t\t\t\t'{ kind: \"text_present\", selector?: string, substr: string } | ' +\n\t\t\t\t'{ kind: \"url_matches\", pattern: string }.',\n\t\t}),\n\t),\n\tvars: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Variables interpolated into the flow ({{var}} placeholders).\",\n\t\t}),\n\t),\n\tstore: Type.Optional(Type.String({ description: \"Path to the evidence store directory for this run.\" })),\n\tlive_view: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Start a live viewer that streams the page and the agent's tool-call log over a local \" +\n\t\t\t\t\"WebSocket, and auto-open it in your default browser. Set HOOCODE_BROWSERTOOLS_NO_OPEN=1 to \" +\n\t\t\t\t\"print the URL without opening. Best for flows that suspend or run long.\",\n\t\t}),\n\t),\n\theadful: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Launch a real on-screen Chromium window instead of a headless browser. Requires a desktop \" +\n\t\t\t\t\"display; unlike live_view it does not show the tool-call log.\",\n\t\t}),\n\t),\n});\n\ntype BrowserRunInput = Static<typeof browserRunSchema>;\n\n/** Structured details surfaced alongside the model-facing content. */\nexport interface BrowserRunDetails {\n\tstatus: \"complete\" | \"needs_parent\";\n\t/** Present when status is \"needs_parent\": resume with this token. */\n\ttoken?: ResumeToken;\n\t/** Present when status is \"needs_parent\": the kind of parent request. */\n\trequestKind?: ParentRequest[\"request\"];\n\t/** Present when status is \"complete\": the flow result/evidence. */\n\tresult?: unknown;\n}\n\nexport interface BrowserRunToolOptions extends BrowsertoolsToolOptions {}\n\n/** ParentResponse shape hint per ParentRequest kind (mirrors contract.rs). */\nfunction parentResponseHint(kind: ParentRequest[\"request\"]): string {\n\tswitch (kind) {\n\t\tcase \"classify_state\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"state\", \"state\": \"<your label>\" }';\n\t\tcase \"verify_visual\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"verified\", \"passed\": true | false }';\n\t\tcase \"extract_semantic\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"extracted\", \"fields\": { \"<field>\": \"<value>\", ... } }';\n\t\tcase \"decide_next_action\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"next_action\", \"action\": <action object> }';\n\t\tcase \"reidentify_element\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"element\", \"selector\": \"<css selector>\" }';\n\t\tdefault:\n\t\t\treturn \"Reply with browser_continue providing the appropriate ParentResponse object.\";\n\t}\n}\n\n/** A control fact from the browsertools observation (mirrors observe.rs InputFact). */\ninterface ObservationInput {\n\tkind?: string;\n\taccessible_name?: string;\n\tselector_hint?: string;\n}\n\n/** A landmark fact from the browsertools observation (mirrors observe.rs Landmark). */\ninterface ObservationLandmark {\n\trole?: string;\n\tname?: string;\n}\n\n/** The HTML-derived observation the engine attaches to decide/classify requests. */\ninterface Observation {\n\turl?: string;\n\ttitle?: string;\n\tinputs?: ObservationInput[];\n\tlandmarks?: ObservationLandmark[];\n\ttext_blocks?: string[];\n\thas_error_region?: boolean;\n}\n\n/** Truncate a string to `max` chars with an ellipsis, for compact rendering. */\nfunction clip(s: string, max = 80): string {\n\tconst t = s.trim();\n\treturn t.length > max ? `${t.slice(0, max - 1)}\\u2026` : t;\n}\n\n/** Render one observation control as a single compact line. */\nfunction renderControl(i: ObservationInput): string {\n\tconst sel = i.selector_hint && i.selector_hint !== \"button\" ? i.selector_hint : undefined;\n\tconst name = i.accessible_name ? `\"${clip(i.accessible_name, 60)}\"` : undefined;\n\tconst parts = [i.kind, name, sel].filter(Boolean);\n\treturn ` - ${parts.join(\" \")}`;\n}\n\n/**\n * Summarize a verbose `decide_next_action`/`classify_state` observation into a\n * compact, readable block instead of dumping the raw JSON (which on busy pages is\n * thousands of mostly-empty entries straight into the model context). The\n * screenshot already conveys the page; this is just the actionable text layer:\n * url/title, named controls, salient headings. Returns undefined when there is no\n * observation to summarize.\n */\nfunction summarizeObservation(observation: Observation | undefined): string | undefined {\n\tif (!observation || typeof observation !== \"object\") return undefined;\n\tconst lines: string[] = [];\n\tif (observation.title) lines.push(`page: ${clip(observation.title, 100)}`);\n\tif (observation.url) lines.push(`url: ${clip(observation.url, 120)}`);\n\tif (observation.has_error_region) lines.push(\"note: page has an error/alert region\");\n\n\tconst controls = (observation.inputs ?? []).filter((i) => i.kind !== \"_more\");\n\tif (controls.length) {\n\t\tlines.push(`controls (${controls.length}):`);\n\t\tfor (const c of controls.slice(0, 25)) lines.push(renderControl(c));\n\t\tif (controls.length > 25) lines.push(` - …+${controls.length - 25} more`);\n\t}\n\n\tconst text = (observation.text_blocks ?? []).filter((t) => t?.trim());\n\tif (text.length) {\n\t\tlines.push(\n\t\t\t`headings: ${text\n\t\t\t\t.slice(0, 12)\n\t\t\t\t.map((t) => clip(t, 50))\n\t\t\t\t.join(\" · \")}`,\n\t\t);\n\t}\n\treturn lines.length ? lines.join(\"\\n\") : undefined;\n}\n\n/**\n * Build the compact, model-facing text for a `NeedsParent` suspension. Renders\n * only the fields that matter per request kind (goal, expected_state, fields,\n * description) plus a summarized observation, rather than pretty-printing the\n * entire request — which is the dominant token sink for browser flows.\n */\nfunction formatParentRequest(request: ParentRequest): string {\n\tconst kind = request.request;\n\tconst lines: string[] = [`request: ${kind}`];\n\tconst r = request as ParentRequest & {\n\t\tgoal?: string;\n\t\texpected_state?: string;\n\t\tdescription?: string;\n\t\tfields?: string[];\n\t\tobservation?: Observation;\n\t};\n\tif (r.goal) lines.push(`goal: ${r.goal}`);\n\tif (r.expected_state) lines.push(`expected_state: ${r.expected_state}`);\n\tif (r.description) lines.push(`element: ${r.description}`);\n\tif (Array.isArray(r.fields) && r.fields.length) lines.push(`fields: ${r.fields.join(\", \")}`);\n\tconst summary = summarizeObservation(r.observation);\n\tif (summary) lines.push(summary);\n\treturn lines.join(\"\\n\");\n}\n\n/** Compact, model-facing reminder of the inline-flow action schema, appended to\n * an `invalid inline flow` error so the model can self-correct in one turn\n * instead of guessing field shapes across several rounds. */\nconst FLOW_SCHEMA_HINT =\n\t\"Inline flow shape: { id, name, version, start_url, steps: [{ id, action }] }. \" +\n\t\"Action variants (use EXACTLY these fields): \" +\n\t'{ action: \"navigate\", url } | { action: \"click\", selector, fallbacks? } | ' +\n\t'{ action: \"fill\", selector, value_tpl } | { action: \"select\", selector, value_tpl } | ' +\n\t'{ action: \"wait_settle\" } | { action: \"checkpoint\", asserts } | ' +\n\t'{ action: \"decide\", goal } | { action: \"classify\" } | ' +\n\t'{ action: \"verify_visual\", expected_state } | { action: \"extract_semantic\", fields }. ' +\n\t'`extract_semantic.fields` MUST be a string array like [\"person\",\"count\"] (not an object), ' +\n\t\"and it takes no `goal` — only `decide` does.\";\n\n/** If an error is a browsertools inline-flow validation failure, append the\n * schema hint so the model fixes the flow on the next call. Other errors pass\n * through unchanged. */\nfunction enrichFlowError(error: unknown): unknown {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (message.includes(\"invalid inline flow\") && !message.includes(\"Action variants\")) {\n\t\treturn new Error(`${message}\\n\\n${FLOW_SCHEMA_HINT}`);\n\t}\n\treturn error;\n}\n\n/** Best-effort: open a URL in the OS default browser. Never throws. Suppressed by\n * HOOCODE_BROWSERTOOLS_NO_OPEN (the URL is still surfaced to the agent). */\nfunction openInBrowser(url: string): boolean {\n\tconst suppress = process.env.HOOCODE_BROWSERTOOLS_NO_OPEN?.trim();\n\tif (suppress === \"1\" || suppress?.toLowerCase() === \"true\") return false;\n\tconst openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n\ttry {\n\t\texec(`${openCmd} \"${url}\"`);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Start the streamed live viewer and auto-open it. Best-effort: a failure here\n * must not abort the flow, so it degrades to returning undefined. Returns a\n * human-readable status line to prepend to the tool result, or undefined.\n *\n * When the serve client is reused across browser_run calls (the\n * reclaimed-idle path), the viewer is already running on the same port, so we\n * skip re-issuing `live_view_start` and skip auto-opening a second OS tab —\n * the user keeps the one tab they already have. */\nasync function startLiveView(client: BrowsertoolsServeClient): Promise<string | undefined> {\n\tif (client.liveViewUrl) {\n\t\treturn `Live view already open at: ${client.liveViewUrl}`;\n\t}\n\ttry {\n\t\tconst result = await client.request<{ url?: string; error?: string }>(\"live_view_start\", {});\n\t\tif (!result?.url) return undefined;\n\t\tclient.liveViewUrl = result.url;\n\t\tconst opened = openInBrowser(result.url);\n\t\treturn opened ? `Live view opened in your browser: ${result.url}` : `Live view available at: ${result.url}`;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Fetch the suspension screenshot for a ParentRequest as an ImageContent block.\n * Best-effort: a fetch failure degrades to no image rather than failing the flow. */\nasync function fetchScreenshot(\n\tclient: BrowsertoolsServeClient,\n\trequest: ParentRequest,\n): Promise<ImageContent | undefined> {\n\tif (!request.screenshot_ref) return undefined;\n\ttry {\n\t\tconst resource = await client.request<GetResourceResult>(\"get_resource\", { ref: request.screenshot_ref });\n\t\tif (!resource?.png_base64) return undefined;\n\t\treturn { type: \"image\", data: resource.png_base64, mimeType: resource.mime || \"image/png\" };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Map a `flow_start`/`flow_resume` outcome to a tool result, owning the live\n * client: dispose it on terminal outcomes, or park it under the new resume token\n * when the flow suspends again. `rounds` is the number of NeedsParent yields seen\n * so far for this flow (including the one being processed), used for the cap.\n */\nexport async function advanceFlow(\n\tclient: BrowsertoolsServeClient,\n\toutcome: FlowOutcome,\n\trounds: number,\n\topts: ReturnType<typeof resolveBrowsertoolsOptions>,\n\tbrowserConfig?: BrowserClientConfig,\n): Promise<AgentToolResult<BrowserRunDetails>> {\n\tif (outcome.outcome === \"complete\") {\n\t\tif (browserConfig) {\n\t\t\tparkIdleClient(client, browserConfig.headful, browserConfig.browserPath, browserConfig.idleTimeoutMs);\n\t\t} else {\n\t\t\tclient.dispose();\n\t\t}\n\t\tconst result = outcome.result;\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: `Flow complete.\\n${JSON.stringify(result ?? {}, null, 2)}` }],\n\t\t\tdetails: { status: \"complete\", result },\n\t\t};\n\t}\n\n\tif (outcome.outcome === \"failed\") {\n\t\tif (browserConfig) {\n\t\t\tparkIdleClient(client, browserConfig.headful, browserConfig.browserPath, browserConfig.idleTimeoutMs);\n\t\t} else {\n\t\t\tclient.dispose();\n\t\t}\n\t\tconst where = outcome.step_id ? ` at step \"${outcome.step_id}\"` : \"\";\n\t\tconst kind = outcome.kind ? ` (${outcome.kind})` : \"\";\n\t\tthrow new Error(`browsertools flow failed${where}: ${outcome.detail ?? \"unknown error\"}${kind}`);\n\t}\n\n\tif (outcome.outcome === \"needs_parent\") {\n\t\tif (rounds > opts.maxParentRounds) {\n\t\t\tclient.dispose();\n\t\t\tthrow new Error(\n\t\t\t\t`browsertools flow exceeded the maximum of ${opts.maxParentRounds} NeedsParent rounds; aborting to avoid a runaway loop`,\n\t\t\t);\n\t\t}\n\t\tconst { request, token } = outcome;\n\t\tconst image = await fetchScreenshot(client, request);\n\t\tparkSession(\n\t\t\ttoken,\n\t\t\tclient,\n\t\t\trounds,\n\t\t\tbrowserConfig?.idleTimeoutMs ?? opts.idleTimeoutMs,\n\t\t\tbrowserConfig?.headful ?? false,\n\t\t\tbrowserConfig?.browserPath,\n\t\t);\n\n\t\tconst text =\n\t\t\t`Flow suspended — parent decision required (NeedsParent).\\n` +\n\t\t\t`${formatParentRequest(request)}\\n` +\n\t\t\t`resume token: ${token}\\n` +\n\t\t\t`${parentResponseHint(request.request)}\\n` +\n\t\t\t(image ? \"A screenshot of the current page is attached.\" : \"(no screenshot available)\");\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (image) content.push(image);\n\t\treturn {\n\t\t\tcontent,\n\t\t\tdetails: { status: \"needs_parent\", token, requestKind: request.request },\n\t\t};\n\t}\n\n\tclient.dispose();\n\tthrow new Error(`browsertools returned an unrecognized flow outcome: ${JSON.stringify(outcome)}`);\n}\n\nexport function createBrowserRunToolDefinition(\n\tcwd: string,\n\toptions?: BrowserRunToolOptions,\n): ToolDefinition<typeof browserRunSchema, BrowserRunDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_run\",\n\t\tlabel: \"browser flow\",\n\t\tdescription:\n\t\t\t\"Start a deterministic browser flow (browsertools). Runs a saved .flow.json (or inline flow) \" +\n\t\t\t\"against a headless browser and returns the evidence on completion. If the flow needs an LLM \" +\n\t\t\t\"decision mid-replay (classify a page state, verify a visual, extract a value, decide the next \" +\n\t\t\t\"action, or re-identify a drifted element) it suspends and returns a typed request plus a \" +\n\t\t\t\"screenshot; answer it with the browser_continue tool using the returned token. Off by default; \" +\n\t\t\t\"enabled with --enable-browsertools.\\n\\n\" +\n\t\t\t\"AGENTIC LOOP (preferred for exploration): for any task where you must read or navigate based on \" +\n\t\t\t\"page content, build the flow from `decide`/`extract_semantic`/`classify`/`verify_visual` steps. \" +\n\t\t\t\"Each such step SUSPENDS and hands you a screenshot of the current page. Read the screenshot, then \" +\n\t\t\t\"call browser_continue with the next action, and keep looping until the outcome is `complete`. Do \" +\n\t\t\t\"NOT fall back to webfetch/curl to read page content you could read from the screenshot — that \" +\n\t\t\t\"bypasses the live session and breaks on auth-gated or JS-rendered pages. A flow ENDS as soon as \" +\n\t\t\t\"its last step runs, so chain several `decide` steps (interleaved with `wait_settle`) when you \" +\n\t\t\t\"need a multi-step journey (search -> open result -> scroll -> extract).\\n\\n\" +\n\t\t\t\"RESUME RESPONSE SHAPES (browser_continue `response` field): decide_next_action -> \" +\n\t\t\t'{ response: \"next_action\", action: <Action> }; classify_state -> { response: \"state\", state: \"<label>\" }; ' +\n\t\t\t'verify_visual -> { response: \"verified\", passed: true|false }; extract_semantic -> ' +\n\t\t\t'{ response: \"extracted\", fields: { <field>: <value> } }; reidentify_element -> ' +\n\t\t\t'{ response: \"element\", selector: \"<css>\" }.\\n\\n' +\n\t\t\t\"ACTION (for next_action) is the same shape as a flow step's action: { action: 'navigate', url }, \" +\n\t\t\t\"{ action: 'click', selector, fallbacks?: string[] }, { action: 'fill', selector, value_tpl }, \" +\n\t\t\t\"{ action: 'select', selector, value_tpl }, { action: 'wait_settle' }. Prefer stable CSS/id \" +\n\t\t\t\"selectors, and ALWAYS pass a `fallbacks` array of alternate selectors for click/fill, because \" +\n\t\t\t\"the primary selector often drifts (e.g. click '.suggestion-link' with fallbacks \" +\n\t\t\t\"['a.mw-searchSuggest-link', '#typeahead-suggestions a']).\\n\\n\" +\n\t\t\t\"VISIBILITY: pass headful:true to launch a real on-screen browser window the user can watch; \" +\n\t\t\t\"live_view:true additionally streams a mirror + tool-call log to a local URL (set live_view:false \" +\n\t\t\t\"to suppress the mirror when the instance defaults it on).\",\n\t\tpromptSnippet: \"Run a deterministic browser flow, pausing for LLM decisions when needed\",\n\t\tparameters: browserRunSchema,\n\t\tasync execute(_toolCallId, params: BrowserRunInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tif (!params.flow_path && !params.flow) {\n\t\t\t\tthrow new Error(\"browser_run requires either `flow_path` or `flow`\");\n\t\t\t}\n\n\t\t\tconst binaryPath = await resolveBrowsertoolsBinary(options);\n\t\t\tconst headful = params.headful ?? opts.headful;\n\t\t\t// Reuse the shared idle client if browser config matches, otherwise create a new\n\t\t\t// one. This keeps a single Chromium process and live-view port across calls.\n\t\t\tlet client = takeIdleClient(headful, opts.browserPath, opts.idleTimeoutMs);\n\t\t\tif (!client) {\n\t\t\t\tclient = new BrowsertoolsServeClient(binaryPath, {\n\t\t\t\t\tcwd,\n\t\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\t\tserveArgs: opts.serveArgs,\n\t\t\t\t\trequestTimeoutMs: opts.requestTimeoutMs,\n\t\t\t\t\theadful,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// If the call is aborted before we hand the client to the registry, make\n\t\t\t// sure the serve process is torn down.\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst startParams: Record<string, unknown> = {};\n\t\t\t\tif (params.flow_path) startParams.flow_path = params.flow_path;\n\t\t\t\tif (params.flow) startParams.flow = params.flow;\n\t\t\t\tif (params.vars) startParams.vars = params.vars;\n\t\t\t\tif (params.store) startParams.store = params.store;\n\n\t\t\t\t// Bring up the live viewer before the flow runs so the page render and\n\t\t\t\t// tool-call log are visible from the first step. The per-call param wins\n\t\t\t\t// over the instance default (--enable-browser-live-preview).\n\t\t\t\tconst liveViewEnabled = params.live_view ?? opts.liveView;\n\t\t\t\tconst liveViewStatus = liveViewEnabled ? await startLiveView(client) : undefined;\n\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_start\", startParams);\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\tconst browserConfig: BrowserClientConfig = {\n\t\t\t\t\theadful,\n\t\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\t\tidleTimeoutMs: opts.idleTimeoutMs,\n\t\t\t\t};\n\t\t\t\tconst result = await advanceFlow(client, outcome, 1, opts, browserConfig);\n\t\t\t\tif (liveViewStatus) {\n\t\t\t\t\tresult.content.unshift({ type: \"text\", text: liveViewStatus });\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow enrichFlowError(error);\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserRunTool(cwd: string, options?: BrowserRunToolOptions): AgentTool<typeof browserRunSchema> {\n\treturn wrapToolDefinition(createBrowserRunToolDefinition(cwd, options));\n}\n"]}
|