agentworkshop 0.7.24 → 0.7.27
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/2jaJHWIJ.js +1 -0
- package/.output/public/_nuxt/B7gZokg82.js +209 -0
- package/.output/public/_nuxt/{Dfe8vXdG.js → B8kt898d.js} +1 -1
- package/.output/public/_nuxt/{BvNJUu3s.js → BFGpEo8S.js} +1 -1
- package/.output/public/_nuxt/{BAfp6tmu.js → BG4zduPf.js} +5 -5
- package/.output/public/_nuxt/{BMb80_tm.js → BGhOrgle.js} +1 -1
- package/.output/public/_nuxt/{Bc-_IsBP.js → BHcpzI2n.js} +1 -1
- package/.output/public/_nuxt/{Djir5rSD.js → BTLEvDxM.js} +1 -1
- package/.output/public/_nuxt/{b50gYVZl.js → BWyR3awA.js} +1 -1
- package/.output/public/_nuxt/Bc89zKq2.js +1 -0
- package/.output/public/_nuxt/{CxY6LfeN.js → BetmGSO4.js} +1 -1
- package/.output/public/_nuxt/{DAR70Lf52.js → BgCrZKv_2.js} +1 -1
- package/.output/public/_nuxt/{DUToq-k_.js → BowTbasd.js} +1 -1
- package/.output/public/_nuxt/{D6HDb-vN2.js → BvtknqSC2.js} +1 -1
- package/.output/public/_nuxt/{CLZWFhZC.js → CTfqWGdy.js} +1 -1
- package/.output/public/_nuxt/Chc5pfAA.js +1 -0
- package/.output/public/_nuxt/CobSUzQv.js +2 -0
- package/.output/public/_nuxt/{kZV5TQvh2.js → CucID8qg2.js} +1 -1
- package/.output/public/_nuxt/{CAGL7TNK.js → DHbTyoAV.js} +1 -1
- package/.output/public/_nuxt/{DSa2O56y.js → DfLUN8kY.js} +1 -1
- package/.output/public/_nuxt/{Coik560n.js → DjU9Dx5Y.js} +1 -1
- package/.output/public/_nuxt/DnziMlcX.js +1 -0
- package/.output/public/_nuxt/{Ct68Z0232.js → DosOJEmZ2.js} +1 -1
- package/.output/public/_nuxt/{CDLv-n51.js → Q_SJDSgx.js} +1 -1
- package/.output/public/_nuxt/{TownView.zu2rU5nS.css → TownView.CtskuMXS.css} +1 -1
- package/.output/public/_nuxt/{CuVGPuP1.js → XY2z78rj.js} +1 -1
- package/.output/public/_nuxt/_id_.DdbYeLqn.css +1 -0
- package/.output/public/_nuxt/{C1CSiSBN.js → bFnZa2yd.js} +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/f7a8e33d-5422-45ac-895d-27168f97e3ad.json +1 -0
- package/.output/public/_nuxt/daq.DGV9QS8d.css +1 -0
- package/.output/public/_nuxt/{entry.AZM1Culo.css → entry.BN02X8iR.css} +1 -1
- package/.output/public/_nuxt/{X6IraCTB.js → rkVgzg62.js} +1 -1
- package/.output/public/_nuxt/settings.W6qrYAui.css +1 -0
- package/.output/public/_nuxt/teams.BBXVkyKq.css +1 -0
- package/.output/public/assets/game/character/yangyang-walk.glb +0 -0
- package/.output/server/chunks/_/host-bindings.mjs +1 -1
- package/.output/server/chunks/_/nitro.mjs +1013 -844
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/build/{TownView-DxeQe1Bl.mjs → TownView-ftIylqiD.mjs} +178 -151
- package/.output/server/chunks/build/{TownView-DxeQe1Bl.mjs.map → TownView-ftIylqiD.mjs.map} +1 -1
- package/.output/server/chunks/build/{_id_-DQDqj-cZ.mjs → _id_-3Axz2-gV.mjs} +35 -35
- package/.output/server/chunks/build/{_id_-DQDqj-cZ.mjs.map → _id_-3Axz2-gV.mjs.map} +1 -1
- package/.output/server/chunks/build/{_id_-DY6v6sqB.mjs → _id_-CahfkmEt.mjs} +5 -5
- package/.output/server/chunks/build/{_id_-DY6v6sqB.mjs.map → _id_-CahfkmEt.mjs.map} +1 -1
- package/.output/server/chunks/build/{_wsId_-D38Vx6Ky.mjs → _wsId_-BymlQhzS.mjs} +7 -7
- package/.output/server/chunks/build/{_wsId_-D38Vx6Ky.mjs.map → _wsId_-BymlQhzS.mjs.map} +1 -1
- package/.output/server/chunks/build/{agents-GnmUGZir.mjs → agents-DuMiSWKp.mjs} +4 -4
- package/.output/server/chunks/build/{agents-GnmUGZir.mjs.map → agents-DuMiSWKp.mjs.map} +1 -1
- package/.output/server/chunks/build/{channel-templates-B4z6v8Z5.mjs → channel-templates-BStqJ-of.mjs} +2 -2
- package/.output/server/chunks/build/{channel-templates-B4z6v8Z5.mjs.map → channel-templates-BStqJ-of.mjs.map} +1 -1
- package/.output/server/chunks/build/{checkbox-dzcpU_Cd.mjs → checkbox-C1gABOca.mjs} +4 -4
- package/.output/server/chunks/build/{checkbox-dzcpU_Cd.mjs.map → checkbox-C1gABOca.mjs.map} +1 -1
- package/.output/server/chunks/build/{daq-B-3xCp_2.mjs → daq-ChYCABz1.mjs} +312 -426
- package/.output/server/chunks/build/daq-ChYCABz1.mjs.map +1 -0
- package/.output/server/chunks/build/{dcw-BL2dhuz7.mjs → dcw-CcQNGggh.mjs} +3 -3
- package/.output/server/chunks/build/{dcw-BL2dhuz7.mjs.map → dcw-CcQNGggh.mjs.map} +1 -1
- package/.output/server/chunks/build/{default-CLf5m0NE.mjs → default-95-zOTPt.mjs} +2 -2
- package/.output/server/chunks/build/{default-CLf5m0NE.mjs.map → default-95-zOTPt.mjs.map} +1 -1
- package/.output/server/chunks/build/{logs-Dto_umFY.mjs → logs-D-2UyGtE.mjs} +3 -3
- package/.output/server/chunks/build/{logs-Dto_umFY.mjs.map → logs-D-2UyGtE.mjs.map} +1 -1
- package/.output/server/chunks/build/{monitor-Cp4V-H2n.mjs → monitor-mfG5jBtz.mjs} +3 -3
- package/.output/server/chunks/build/{monitor-Cp4V-H2n.mjs.map → monitor-mfG5jBtz.mjs.map} +1 -1
- package/.output/server/chunks/build/{pages-Dc2H1QFu.mjs → pages-Dfosfchr.mjs} +4 -4
- package/.output/server/chunks/build/{pages-Dc2H1QFu.mjs.map → pages-Dfosfchr.mjs.map} +1 -1
- package/.output/server/chunks/build/{permissions-BTRW2CVd.mjs → permissions-B-hhZuzm.mjs} +3 -3
- package/.output/server/chunks/build/{permissions-BTRW2CVd.mjs.map → permissions-B-hhZuzm.mjs.map} +1 -1
- package/.output/server/chunks/build/{settings-UfGyDgM7.mjs → settings-BajN9TQW.mjs} +249 -20
- package/.output/server/chunks/build/settings-BajN9TQW.mjs.map +1 -0
- package/.output/server/chunks/build/{table-Cswq-l2r.mjs → table-Bf1r2JjE.mjs} +2 -2
- package/.output/server/chunks/build/{table-Cswq-l2r.mjs.map → table-Bf1r2JjE.mjs.map} +1 -1
- package/.output/server/chunks/build/{teams-Cbg27MWW.mjs → teams-t2SkRlJP.mjs} +346 -46
- package/.output/server/chunks/build/teams-t2SkRlJP.mjs.map +1 -0
- package/.output/server/chunks/build/{tokens-Bmm6rDfj.mjs → tokens-DiCojbN8.mjs} +3 -3
- package/.output/server/chunks/build/{tokens-Bmm6rDfj.mjs.map → tokens-DiCojbN8.mjs.map} +1 -1
- package/.output/server/chunks/build/{town-Dsh0jlwZ.mjs → town-C7QM8DYK.mjs} +5 -5
- package/.output/server/chunks/build/{town-Dsh0jlwZ.mjs.map → town-C7QM8DYK.mjs.map} +1 -1
- package/.output/server/chunks/build/{useDaqStream-DkRL3tEW.mjs → useDaqStream-BeA2X8KR.mjs} +26 -11
- package/.output/server/chunks/build/useDaqStream-BeA2X8KR.mjs.map +1 -0
- package/.output/server/chunks/build/{useDcwStream-DyB2ufQI.mjs → useDcwStream-BJW_MitY.mjs} +14 -6
- package/.output/server/chunks/build/useDcwStream-BJW_MitY.mjs.map +1 -0
- package/.output/server/chunks/build/{useDeviceTwins-ZbT86L9O.mjs → useDeviceTwins-Dwz_EY_i.mjs} +2 -2
- package/.output/server/chunks/build/{useDeviceTwins-ZbT86L9O.mjs.map → useDeviceTwins-Dwz_EY_i.mjs.map} +1 -1
- package/.output/server/chunks/build/{useOpsLog-8HCYFb6n.mjs → useOpsLog-Dm8QEb1I.mjs} +2 -2
- package/.output/server/chunks/build/{useOpsLog-8HCYFb6n.mjs.map → useOpsLog-Dm8QEb1I.mjs.map} +1 -1
- package/.output/server/chunks/build/{useWorkshopApi-D2gq06N2.mjs → useWorkshopApi-B-L3MmIX.mjs} +9 -2
- package/.output/server/chunks/build/{useWorkshopApi-D2gq06N2.mjs.map → useWorkshopApi-B-L3MmIX.mjs.map} +1 -1
- package/.output/server/chunks/build/{users-B50HHrfi.mjs → users-B9SAdEtH.mjs} +3 -3
- package/.output/server/chunks/build/{users-B50HHrfi.mjs.map → users-B9SAdEtH.mjs.map} +1 -1
- package/.output/server/chunks/routes/api/plugins/_name/_...path_.mjs +14 -2
- package/.output/server/chunks/routes/api/plugins/_name/_...path_.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/channels/_id/index.put.mjs +40 -0
- package/.output/server/chunks/routes/api/workshop/channels/_id/index.put.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/channels/_id/index4.get.mjs +21 -9
- package/.output/server/chunks/routes/api/workshop/channels/_id/index4.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channels/_id/index5.get.mjs +33 -0
- package/.output/server/chunks/routes/api/workshop/channels/_id/index5.get.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/channels/_id/terminals.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/channels.post.mjs +15 -1
- package/.output/server/chunks/routes/api/workshop/channels.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id/bind.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id/bindings.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id/frames/content.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id/frames.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id/samples.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id/test.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/_id_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/alarms/_id/ack.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/controller.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/index.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/infra/reconnect.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/infra.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/daq/test-driver.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/bind.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/bindings.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/param-ledger.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/read.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/test.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id/write.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/_id_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/controller.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index2.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index2.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index3.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index3.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index4.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index4.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/index5.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/journal/node/_nodeId/rollback.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/line/query.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/start.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/stop.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/judge.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/rollback.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/series.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id_.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/products/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/products/_id_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/apply.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/mark-good.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/rollback-good.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/close.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/data.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/rollback.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/test-driver.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/device-twins/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/device-twins/_id_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index3.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index4.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index4.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index5.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index5.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index6.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
- package/.output/server/chunks/routes/renderer.mjs +2 -2
- package/.output/server/chunks/virtual/entry.mjs +352 -433
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/app/components/workshop/DaqNodeRow.vue +398 -0
- package/app/components/workshop/town/ModelPreview3D.vue +26 -4
- package/app/components/workshop/town/TownScene3D.ts +60 -12
- package/app/components/workshop/town/TownView.vue +4 -3
- package/app/composables/workshop/useCharacterAssets.ts +1 -0
- package/app/composables/workshop/useDaqStream.ts +24 -5
- package/app/composables/workshop/useDcwStream.ts +13 -4
- package/app/composables/workshop/useWorkshopApi.ts +294 -259
- package/app/pages/daq/[id].vue +27 -3
- package/app/pages/daq/index.vue +8 -355
- package/app/pages/settings.vue +253 -2
- package/app/pages/workshop/teams.vue +212 -2
- package/i18n/dicts/_en.json +361 -1
- package/i18n/dicts/plugins.json +10 -0
- package/i18n/dicts/settings.json +1 -0
- package/i18n/dicts/teamPlugins.json +10 -0
- package/i18n/locales/en.ts +1860 -1900
- package/i18n/locales/zh-CN.ts +1788 -1829
- package/package.json +149 -148
- package/public/assets/game/character/yangyang-walk.glb +0 -0
- package/scripts/_dbg-cpu-profile.mjs +53 -0
- package/scripts/_dbg-render-perf.mjs +142 -0
- package/scripts/_dbg-render-regression.mjs +130 -0
- package/scripts/i18n-merge.mjs +152 -30
- package/scripts/production-closed-loop-e2e.mjs +254 -0
- package/scripts/team-plugins-api-smoke.mjs +149 -0
- package/scripts/three-system-e2e.mjs +448 -0
- package/server/api/plugins/[name]/[...path].ts +46 -31
- package/server/api/workshop/channels/[id]/plugins/index.get.ts +35 -0
- package/server/api/workshop/channels/[id]/plugins/index.put.ts +39 -0
- package/server/api/workshop/channels.post.ts +17 -0
- package/server/data/dcw-lines.json +7 -0
- package/server/data/dcw-products.json +7 -0
- package/server/data/dcw-recipes.json +1 -1
- package/server/data/dcw-rollback.json +523 -11
- package/server/data/dcw-writes.json +72 -0
- package/server/data/dcws.json +368 -368
- package/server/data/scene-layouts.json +66 -0
- package/server/plugins/aw-plugins.ts +35 -27
- package/server/plugins-builtin/diag-bridge/index.mjs +556 -0
- package/server/plugins-builtin/rag-bridge/index.mjs +364 -0
- package/server/services/workshop/agents/host-tool-bridge.ts +659 -642
- package/server/services/workshop/agents/omp-agent.ts +1178 -1178
- package/server/services/workshop/agents/plugin-tools.ts +10 -1
- package/server/services/workshop/db/channel-plugins.repo.ts +65 -0
- package/server/services/workshop/plugins/host.mjs +52 -4
- package/server/services/workshop/runtime/manager.ts +2976 -2968
- package/shared/config/schema.json +4 -0
- package/.output/public/_nuxt/BUcmySDa2.js +0 -209
- package/.output/public/_nuxt/CYzpZzB_.js +0 -1
- package/.output/public/_nuxt/CjXIpPMR.js +0 -1
- package/.output/public/_nuxt/DKHcFE1i.js +0 -1
- package/.output/public/_nuxt/DjtE7AYt.js +0 -1
- package/.output/public/_nuxt/JOwsShod.js +0 -2
- package/.output/public/_nuxt/_id_.C-YXjn8Q.css +0 -1
- package/.output/public/_nuxt/builds/meta/024de5f7-6a10-4f53-82ab-bc65f3ec9e35.json +0 -1
- package/.output/public/_nuxt/daq.3OKj58HH.css +0 -1
- package/.output/public/_nuxt/settings.BqMF2nfj.css +0 -1
- package/.output/public/_nuxt/teams.DN0pAGGu.css +0 -1
- package/.output/server/chunks/build/daq-B-3xCp_2.mjs.map +0 -1
- package/.output/server/chunks/build/settings-UfGyDgM7.mjs.map +0 -1
- package/.output/server/chunks/build/teams-Cbg27MWW.mjs.map +0 -1
- package/.output/server/chunks/build/useDaqStream-DkRL3tEW.mjs.map +0 -1
- package/.output/server/chunks/build/useDcwStream-DyB2ufQI.mjs.map +0 -1
|
@@ -1,1178 +1,1178 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OmpRpcAgentImpl — omp harness 的完整 AgentInterface 实现。
|
|
3
|
-
*
|
|
4
|
-
* 通过 OmpRpcClient 驱动真实 omp 子进程(--mode rpc),将 omp 的原生 agent 能力
|
|
5
|
-
* (LLM 推理 + 内置工具 read/write/edit/bash/grep/glob 等)接入 Workshop 的
|
|
6
|
-
* Channel 编排体系。
|
|
7
|
-
*
|
|
8
|
-
* 核心设计:
|
|
9
|
-
* - 每个 Agent 实例持有一个 omp 子进程(lazy spawn,跨消息复用)
|
|
10
|
-
* - AgentWorkspace 方法经 host-tool-bridge 注册为 omp host tools,agent 原生调用
|
|
11
|
-
* report_progress / complete_task / dispatch_task / send_message 等,无需文本解析
|
|
12
|
-
* - omp AgentSessionEvent → AgentEvent 五变体映射
|
|
13
|
-
* - worker run():接收 assign → prompt omp → agent 用原生工具作业 + host tools 管理任务生命周期
|
|
14
|
-
* - lead supervise():格式化快照 → prompt omp → agent 直接用 host tools 执行调度决策 → 返回 []
|
|
15
|
-
* - dispose():杀子进程
|
|
16
|
-
*
|
|
17
|
-
* prompt 组装与工具分发已抽至共享层(prompt-builder / host-tool-bridge),
|
|
18
|
-
* 与 codex/dsh/opencode impl 全引擎一致。协议权威:omp://rpc.md。
|
|
19
|
-
*/
|
|
20
|
-
import { createLogger } from '../logger'
|
|
21
|
-
import { randomUUID } from 'node:crypto'
|
|
22
|
-
import type {
|
|
23
|
-
AgentEvent,
|
|
24
|
-
AgentInterface,
|
|
25
|
-
AgentInfo,
|
|
26
|
-
AgentRunContext,
|
|
27
|
-
AgentRunRequest,
|
|
28
|
-
SupervisionDecision,
|
|
29
|
-
SupervisionSnapshot,
|
|
30
|
-
} from './agent-interface'
|
|
31
|
-
import type { A2AMessage } from '../types/a2a'
|
|
32
|
-
import type { AgentContextStats } from '../types/task'
|
|
33
|
-
import {
|
|
34
|
-
OmpRpcClient,
|
|
35
|
-
type AgentSessionEvent,
|
|
36
|
-
type CompactionResult,
|
|
37
|
-
type HostToolCallRequest,
|
|
38
|
-
} from './adapters/omp-rpc-client'
|
|
39
|
-
import {
|
|
40
|
-
registerHarnessProcess,
|
|
41
|
-
bindHarnessProcess,
|
|
42
|
-
markHarnessProcessExit,
|
|
43
|
-
killHarnessProcess,
|
|
44
|
-
} from './harness-process'
|
|
45
|
-
import {
|
|
46
|
-
attachTerminalTap,
|
|
47
|
-
markTerminalSessionExit,
|
|
48
|
-
} from './harness-terminal'
|
|
49
|
-
import {
|
|
50
|
-
createSessionState,
|
|
51
|
-
dispatchHostTool,
|
|
52
|
-
hostToolsForRole,
|
|
53
|
-
} from './host-tool-bridge'
|
|
54
|
-
import {
|
|
55
|
-
contextPrefix as buildContextPrefix,
|
|
56
|
-
createRosterCache,
|
|
57
|
-
extractJsonArray,
|
|
58
|
-
parseSteerBanner,
|
|
59
|
-
peerPrompt,
|
|
60
|
-
supervisePrompt,
|
|
61
|
-
systemManual,
|
|
62
|
-
toolArgsPreview,
|
|
63
|
-
workerPrompt,
|
|
64
|
-
} from './prompt-builder'
|
|
65
|
-
import { renderPrompt } from '../prompts/loader'
|
|
66
|
-
import { attachOmpPluginBridge, onPluginToolsChange } from './plugin-tools'
|
|
67
|
-
import { ompSettings } from '../settings'
|
|
68
|
-
|
|
69
|
-
const log = createLogger('workshop.omp')
|
|
70
|
-
|
|
71
|
-
// 兼容再导出(脚本/测试历史导入面;实现在 host-tool-bridge)
|
|
72
|
-
export { HOST_TOOLS, hostToolsForRole } from './host-tool-bridge'
|
|
73
|
-
|
|
74
|
-
// ===== 配置 =====
|
|
75
|
-
|
|
76
|
-
export interface OmpAgentConfig {
|
|
77
|
-
/** omp 可执行文件路径(默认 'omp',从 PATH 查找) */
|
|
78
|
-
command?: string
|
|
79
|
-
/** 额外 CLI 参数 */
|
|
80
|
-
args?: string[]
|
|
81
|
-
/** omp 工作目录(默认 process.cwd()) */
|
|
82
|
-
cwd?: string
|
|
83
|
-
/** 模型 provider(如 'anthropic'/'openai'/'zhipu');省略则用 omp 默认 */
|
|
84
|
-
provider?: string
|
|
85
|
-
/** 模型 ID;省略则用 omp 默认 */
|
|
86
|
-
model?: string
|
|
87
|
-
/** thinking level: off/minimal/low/medium/high/xhigh/max */
|
|
88
|
-
thinkingLevel?: string
|
|
89
|
-
/** 自定义系统 prompt 前缀(拼接到 agent 系统指令之前) */
|
|
90
|
-
systemPromptPrefix?: string
|
|
91
|
-
/** 限制 omp 仅使用指定内置工具(null = 全部) */
|
|
92
|
-
toolNames?: string[]
|
|
93
|
-
/** 每轮停滞超时(ms,默认 300000):整轮无任何 omp 事件才中止;工具内阻塞(poll_messages ≤180s)有 tool 事件刷新计时 */
|
|
94
|
-
promptTimeoutMs?: number
|
|
95
|
-
/** supervise 轮超时(ms,默认 150000) */
|
|
96
|
-
superviseTimeoutMs?: number
|
|
97
|
-
/**
|
|
98
|
-
* omp 输出模式:'rpc-ui'(默认)= RPC 协议 + UI 上下文 —— ask 工具与
|
|
99
|
-
* extension_ui_request 对话框可用(监控终端 HITL 通道,强制 noPty);
|
|
100
|
-
* 'rpc' = 纯协议(无 UI,ask 不可用)。
|
|
101
|
-
*/
|
|
102
|
-
rpcMode?: 'rpc' | 'rpc-ui'
|
|
103
|
-
/** channel 级作业场景 prompt(manager 装配时注入;用户场景规范,全员共享) */
|
|
104
|
-
scenarioPrompt?: string
|
|
105
|
-
/** agent 身份(由 factory 从 AgentInfo 注入) */
|
|
106
|
-
agentId?: string
|
|
107
|
-
name?: string
|
|
108
|
-
role?: 'lead' | 'worker'
|
|
109
|
-
channelId?: string
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 插件桥接管(host.mjs ctx.omp 排队注册的工具在此回放;幂等)
|
|
113
|
-
attachOmpPluginBridge()
|
|
114
|
-
|
|
115
|
-
/** 在跑 agent 实例表(工具注册表变更 → 热重发 set_host_tools,运行时注入无需重spawn) */
|
|
116
|
-
const gLive = globalThis as typeof globalThis & { __ompLiveAgents?: Set<OmpRpcAgentImpl> }
|
|
117
|
-
function liveAgents(): Set<OmpRpcAgentImpl> {
|
|
118
|
-
return gLive.__ompLiveAgents ??= new Set()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 工具注册表变更 → 全部在跑会话热更新工具面
|
|
122
|
-
onPluginToolsChange(() => {
|
|
123
|
-
for (const impl of liveAgents()) {
|
|
124
|
-
try {
|
|
125
|
-
impl.refreshPluginTools()
|
|
126
|
-
}
|
|
127
|
-
catch { /* 单实例失败不影响其他 */ }
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// ===== OmpRpcAgentImpl =====
|
|
132
|
-
|
|
133
|
-
export class OmpRpcAgentImpl implements AgentInterface {
|
|
134
|
-
private readonly config: OmpAgentConfig
|
|
135
|
-
private client: OmpRpcClient | null = null
|
|
136
|
-
private workspace: AgentRunContext['workspace'] | null = null
|
|
137
|
-
private agentInfo: AgentInfo | null = null
|
|
138
|
-
private hostToolsRegistered = false
|
|
139
|
-
/** host tool 会话态(当前任务 + 待回执上下文;与共享桥共用) */
|
|
140
|
-
private readonly toolState = createSessionState()
|
|
141
|
-
/** 工具桥上下文(分发唯一入口) */
|
|
142
|
-
private readonly bridgeCtx = {
|
|
143
|
-
identity: { agentId: '', channelId: '', role: 'worker' as 'lead' | 'worker', name: 'agent' },
|
|
144
|
-
state: this.toolState,
|
|
145
|
-
getWorkspace: () => this.workspace,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* 会话回合状态(steer 可靠注入的依据):
|
|
150
|
-
* omp 的 steer 仅在回合 streaming 中生效——prompt 已入列但尚未开始输出时,
|
|
151
|
-
* steer 会"成功"返回但被静默丢弃。因此注入方轮询直到回合输出开始(streamingStarted)
|
|
152
|
-
* 才发送;若回合在等待期间结束,消息保持 pending 由消费循环处理。
|
|
153
|
-
*/
|
|
154
|
-
private streaming = false
|
|
155
|
-
private turnActive = false
|
|
156
|
-
/** 当前回合产生的 assistant 文本(供诊断) */
|
|
157
|
-
private turnText = ''
|
|
158
|
-
/** agent 身份信息(factory 注入;无需等待 init()) */
|
|
159
|
-
private selfAgentId = ''
|
|
160
|
-
private agentName = 'agent'
|
|
161
|
-
private agentRole: 'lead' | 'worker' = 'worker'
|
|
162
|
-
private channelId = ''
|
|
163
|
-
/** 团队名册缓存(共享 prompt-builder) */
|
|
164
|
-
private roster = createRosterCache({
|
|
165
|
-
selfAgentId: '',
|
|
166
|
-
listAgents: () => this.workspace!.listAgents(),
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
// ===== 上下文治理(70% 无中断压缩环)=====
|
|
170
|
-
/** 压缩进行中(平台 gate 与 omp 原生压缩共用互斥位) */
|
|
171
|
-
private compacting = false
|
|
172
|
-
private lastCompactAt = 0
|
|
173
|
-
/** omp 不支持 compact 命令(旧版)→ 永久停用平台压缩;原生 auto-compaction 兜底,harvest 照常 */
|
|
174
|
-
private compactLegacy = false
|
|
175
|
-
/** 模型上下文窗口(get_state/get_session_stats 探测;null = 未知,percent 不可算) */
|
|
176
|
-
private contextWindow: number | null = null
|
|
177
|
-
private sessionId: string | null = null
|
|
178
|
-
/** harvest 双路去重(compact 响应与 compaction_end 事件可能双达) */
|
|
179
|
-
private lastHarvestKey = ''
|
|
180
|
-
private lastHarvestAt = 0
|
|
181
|
-
|
|
182
|
-
/** 总开关(omp.compact_enabled;env AW_OMP_COMPACT_ENABLED=0 显式关闭兼容) */
|
|
183
|
-
private compactEnabled(): boolean {
|
|
184
|
-
return ompSettings().compact_enabled
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private compactThreshold(): number {
|
|
188
|
-
return ompSettings().compact_threshold
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private compactMinIntervalMs(): number {
|
|
192
|
-
return ompSettings().compact_min_interval_ms
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private compactWaitMs(): number {
|
|
196
|
-
return ompSettings().compact_wait_ms
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** 被动上下文用量快照(无探测 RPC;getStatus 透出用) */
|
|
200
|
-
getContextStats(): AgentContextStats | null {
|
|
201
|
-
const usage = this.client?.getContextUsage() ?? null
|
|
202
|
-
const window = this.contextWindow ?? usage?.contextWindow ?? null
|
|
203
|
-
if (!usage && window === null) return null
|
|
204
|
-
const usedTokens = usage?.tokens ?? 0
|
|
205
|
-
return {
|
|
206
|
-
usedTokens,
|
|
207
|
-
contextWindow: window,
|
|
208
|
-
percent: window && window > 0 ? Math.min(1, usedTokens / window) : null,
|
|
209
|
-
compacting: this.compacting,
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* 上下文用量探测:get_session_stats 权威值;不可用退化为被动 usage + 已探测窗口。
|
|
215
|
-
* percent 归一化到 0-1(omp 可能返回 0-100)。
|
|
216
|
-
*/
|
|
217
|
-
private async probeUsage(client: OmpRpcClient): Promise<{ tokens: number, contextWindow: number | null, percent: number | null } | null> {
|
|
218
|
-
try {
|
|
219
|
-
const resp = await client.send({ type: 'get_session_stats' })
|
|
220
|
-
const cu = ((resp.data ?? {}) as Record<string, unknown>).contextUsage as
|
|
221
|
-
| { tokens?: number, contextWindow?: number, percent?: number }
|
|
222
|
-
| undefined
|
|
223
|
-
if (cu && typeof cu.tokens === 'number' && cu.tokens > 0) {
|
|
224
|
-
if (typeof cu.contextWindow === 'number' && cu.contextWindow > 0) {
|
|
225
|
-
this.contextWindow = cu.contextWindow
|
|
226
|
-
client.setContextWindow(cu.contextWindow)
|
|
227
|
-
}
|
|
228
|
-
const window = this.contextWindow ?? cu.contextWindow ?? null
|
|
229
|
-
const rawPercent = typeof cu.percent === 'number' ? (cu.percent > 1 ? cu.percent / 100 : cu.percent) : null
|
|
230
|
-
const percent = rawPercent ?? (window && window > 0 ? Math.min(1, cu.tokens / window) : null)
|
|
231
|
-
return { tokens: cu.tokens, contextWindow: window, percent }
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
catch { /* get_session_stats 不可用 → 被动跟踪 */ }
|
|
235
|
-
return client.getContextUsage()
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* 上下文治理门控:三条 prompt 路径(worker/peer/supervise)在回合间隙统一调用;
|
|
240
|
-
* post-settle 经 onTurnSettled 复用同一守卫。任何失败路径都放行——压缩晚一轮
|
|
241
|
-
* 永远好过中断作业。三重防线:仅回合间隙发起 → get_state 复查 isStreaming/
|
|
242
|
-
* isCompacting → compacting 互斥位 + 最小间隔防振荡。
|
|
243
|
-
*/
|
|
244
|
-
private async contextGate(reason: string): Promise<void> {
|
|
245
|
-
if (!this.compactEnabled() || this.compactLegacy || this.compacting) return
|
|
246
|
-
const client = this.client
|
|
247
|
-
if (!client || !client.alive) return
|
|
248
|
-
try {
|
|
249
|
-
const usage = await this.probeUsage(client)
|
|
250
|
-
if (!usage || usage.percent === null || usage.percent < this.compactThreshold()) return
|
|
251
|
-
if (Date.now() - this.lastCompactAt < this.compactMinIntervalMs()) return
|
|
252
|
-
// 回合间隙硬校验:流式中/压缩中绝不发起(不中断执行的硬约束)
|
|
253
|
-
const st = await client.send({ type: 'get_state' })
|
|
254
|
-
const s = (st.data ?? {}) as Record<string, unknown>
|
|
255
|
-
if (s.isStreaming === true || s.isCompacting === true) return
|
|
256
|
-
if (typeof s.sessionId === 'string' && s.sessionId) this.sessionId = s.sessionId
|
|
257
|
-
this.compacting = true
|
|
258
|
-
this.lastCompactAt = Date.now()
|
|
259
|
-
try {
|
|
260
|
-
const result = await this.runCompaction(client)
|
|
261
|
-
if (result?.summary) {
|
|
262
|
-
await this.harvestCompaction(
|
|
263
|
-
result.summary,
|
|
264
|
-
result.tokensBefore,
|
|
265
|
-
result.tokensAfter ?? result.estimatedTokensAfter,
|
|
266
|
-
reason,
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
finally {
|
|
271
|
-
this.compacting = false
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
276
|
-
if (/compact/i.test(msg) && /unknown|unsupported|invalid|无效|未知|不支持/i.test(msg)) this.compactLegacy = true
|
|
277
|
-
log.warn(`[OmpRpcAgent:${this.selfAgentId}] contextGate(${reason}) 失败,放行: ${msg}`)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* 发起 compact 并等待 compaction_end(响应内含 result 则立即收口;
|
|
283
|
-
* 超时放行——压缩在 harness 内后台继续,isCompacting 防后续双发)。
|
|
284
|
-
*/
|
|
285
|
-
private runCompaction(client: OmpRpcClient): Promise<CompactionResult | null> {
|
|
286
|
-
return new Promise<CompactionResult | null>((resolve) => {
|
|
287
|
-
let settled = false
|
|
288
|
-
const finish = (r: CompactionResult | null): void => {
|
|
289
|
-
if (settled) return
|
|
290
|
-
settled = true
|
|
291
|
-
off()
|
|
292
|
-
clearTimeout(timer)
|
|
293
|
-
resolve(r)
|
|
294
|
-
}
|
|
295
|
-
const off = client.onEvent((event) => {
|
|
296
|
-
if (event.type === 'compaction_end') {
|
|
297
|
-
finish((event as { result?: CompactionResult }).result ?? null)
|
|
298
|
-
}
|
|
299
|
-
else if (event.type === '__process_exit__') {
|
|
300
|
-
finish(null)
|
|
301
|
-
}
|
|
302
|
-
})
|
|
303
|
-
const timer = setTimeout(() => {
|
|
304
|
-
log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 等待超时(${this.compactWaitMs()}ms)→ 放行(压缩后台继续)`)
|
|
305
|
-
finish(null)
|
|
306
|
-
}, this.compactWaitMs())
|
|
307
|
-
void client.send({ type: 'compact', customInstructions: this.compactionHints() })
|
|
308
|
-
.then((resp) => {
|
|
309
|
-
const result = (resp.data as { result?: CompactionResult } | undefined)?.result
|
|
310
|
-
if (result?.summary) finish(result)
|
|
311
|
-
// 响应无 result:等 compaction_end 事件(协议权威语义)
|
|
312
|
-
})
|
|
313
|
-
.catch((err) => {
|
|
314
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
315
|
-
if (/compact/i.test(msg)) this.compactLegacy = true
|
|
316
|
-
log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 发起失败: ${msg}`)
|
|
317
|
-
finish(null)
|
|
318
|
-
})
|
|
319
|
-
})
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** 压缩摘要指令(外置 prompts;缺失时省略,omp 用默认策略) */
|
|
323
|
-
private compactionHints(): string | undefined {
|
|
324
|
-
try {
|
|
325
|
-
return renderPrompt('compaction-hints')
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
return undefined
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* 压缩摘要 harvest(三路统一:平台主动 compact / omp 阈值自动 / overflow):
|
|
334
|
-
* 经 workspace.recordSessionMemory 落库为本人 episodic-session 记忆。
|
|
335
|
-
* 双路去重:同摘要 60s 内只入库一次。
|
|
336
|
-
*/
|
|
337
|
-
private async harvestCompaction(summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string): Promise<void> {
|
|
338
|
-
const key = summary.slice(0, 120)
|
|
339
|
-
if (key === this.lastHarvestKey && Date.now() - this.lastHarvestAt < 60_000) return
|
|
340
|
-
this.lastHarvestKey = key
|
|
341
|
-
this.lastHarvestAt = Date.now()
|
|
342
|
-
try {
|
|
343
|
-
await this.workspace?.recordSessionMemory?.({ summary, tokensBefore, tokensAfter, reason })
|
|
344
|
-
log.info(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要已入记忆(${summary.length} 字,reason=${reason ?? 'auto'})`)
|
|
345
|
-
}
|
|
346
|
-
catch (err) {
|
|
347
|
-
log.warn(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要入库失败(不影响会话):`, err instanceof Error ? err.message : err)
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** 回合落定钩子(AgentRuntime 在信箱空时调用):后台压缩检查,自守卫不抛错 */
|
|
352
|
-
async onTurnSettled(): Promise<void> {
|
|
353
|
-
await this.contextGate('post-settle')
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
constructor(config: Record<string, unknown> = {}) {
|
|
357
|
-
this.config = config as OmpAgentConfig
|
|
358
|
-
// effort 统一入口(channel 默认 LLM 注入 config.effort)→ omp thinking 级别
|
|
359
|
-
if (!this.config.thinkingLevel && typeof config.effort === 'string' && config.effort) {
|
|
360
|
-
this.config.thinkingLevel = config.effort
|
|
361
|
-
}
|
|
362
|
-
// factory 注入的 agent 身份(无需等待 init())
|
|
363
|
-
this.selfAgentId = this.config.agentId ?? ''
|
|
364
|
-
this.agentName = this.config.name ?? 'agent'
|
|
365
|
-
this.agentRole = this.config.role ?? 'worker'
|
|
366
|
-
this.channelId = this.config.channelId ?? ''
|
|
367
|
-
this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
|
|
368
|
-
this.roster = createRosterCache({
|
|
369
|
-
selfAgentId: this.selfAgentId,
|
|
370
|
-
listAgents: () => this.workspace!.listAgents(),
|
|
371
|
-
})
|
|
372
|
-
liveAgents().add(this) // 在跑实例表:插件工具热注入的目标集
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// ===== 生命周期 =====
|
|
376
|
-
|
|
377
|
-
async init(input: { agent: AgentInfo, channelId: string }): Promise<void> {
|
|
378
|
-
this.agentInfo = input.agent
|
|
379
|
-
this.channelId = input.channelId
|
|
380
|
-
this.agentName = input.agent.name
|
|
381
|
-
this.agentRole = input.agent.role
|
|
382
|
-
this.selfAgentId = input.agent.id
|
|
383
|
-
this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
|
|
384
|
-
this.roster = createRosterCache({
|
|
385
|
-
selfAgentId: this.selfAgentId,
|
|
386
|
-
listAgents: () => this.workspace!.listAgents(),
|
|
387
|
-
})
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async dispose(): Promise<void> {
|
|
391
|
-
liveAgents().delete(this)
|
|
392
|
-
if (this.client) {
|
|
393
|
-
const pid = this.client.pid
|
|
394
|
-
await this.client.dispose()
|
|
395
|
-
this.client = null
|
|
396
|
-
if (pid) {
|
|
397
|
-
markHarnessProcessExit(pid, null)
|
|
398
|
-
markTerminalSessionExit(pid, null)
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
this.hostToolsRegistered = false
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/** 工具注册表变更 → 热重发 set_host_tools(在跑会话立即获得插件新工具,无需重spawn) */
|
|
405
|
-
refreshPluginTools(): void {
|
|
406
|
-
const client = this.client
|
|
407
|
-
if (!client || !client.alive) return
|
|
408
|
-
void client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole) }).catch(() => {})
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** harness 进程资源信息(运行时资源监控;进程未 spawn/已回收 → null) */
|
|
412
|
-
getProcessInfo(): { pid: number, alive: boolean, command: string } | null {
|
|
413
|
-
const client = this.client
|
|
414
|
-
const pid = client?.pid
|
|
415
|
-
if (!pid || !client) return null
|
|
416
|
-
return { pid, alive: client.alive, command: `omp --mode ${this.rpcMode}` }
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/** 强制终止 harness 进程(进程树;终止后由调用方停止对应 AgentRuntime) */
|
|
420
|
-
killProcess(): void {
|
|
421
|
-
const pid = this.client?.pid
|
|
422
|
-
if (pid) {
|
|
423
|
-
markTerminalSessionExit(pid, null)
|
|
424
|
-
killHarnessProcess(pid)
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
this.client?.kill()
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* harness 进程存活校准(manager sweeper 周期性调用;休眠/强杀后 exit 事件
|
|
433
|
-
* 可能不达父进程):按 PID 探 OS 实际存在性,进程已死 → 客户端收敛为已退出,
|
|
434
|
-
* 在途回合经 __process_exit__ 归位,下一回合 ensureClient 重生子进程。
|
|
435
|
-
*/
|
|
436
|
-
reconcileProcess(): void {
|
|
437
|
-
this.client?.reconcile()
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/** omp 输出模式(rpc-ui = 默认,启用 HITL 对话框) */
|
|
441
|
-
private get rpcMode(): 'rpc' | 'rpc-ui' {
|
|
442
|
-
return this.config.rpcMode === 'rpc' ? 'rpc' : 'rpc-ui'
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// ===== 工具桥(共享分发)=====
|
|
446
|
-
|
|
447
|
-
/** 引擎无关工具面:REST/MCP 桥直调与 omp 内部 host_tool_call 共用同一实现 */
|
|
448
|
-
dispatchHostTool(toolName: string, args: Record<string, unknown>): Promise<{ text: string, isError?: boolean }> {
|
|
449
|
-
return dispatchHostTool(this.bridgeCtx, { toolName, arguments: args })
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
private async handleHostTool(req: HostToolCallRequest): Promise<{ text: string, isError?: boolean }> {
|
|
453
|
-
return this.dispatchHostTool(req.toolName, req.arguments ?? {})
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// ===== prompt 组合 =====
|
|
457
|
-
|
|
458
|
-
/** 前置上下文(场景 × 身份 × 工业简报 × 名册;共享 prompt-builder) */
|
|
459
|
-
private async contextPrefix(): Promise<string> {
|
|
460
|
-
return buildContextPrefix({
|
|
461
|
-
scenarioPrompt: this.config.scenarioPrompt,
|
|
462
|
-
systemPromptPrefix: this.config.systemPromptPrefix,
|
|
463
|
-
agentId: this.selfAgentId,
|
|
464
|
-
roster: await this.roster.roster(),
|
|
465
|
-
})
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
private systemManual(): string {
|
|
469
|
-
return systemManual()
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* 实时消息注入(送达模式协议):
|
|
474
|
-
* - 回合 streaming 中 → 立即 steer,返回 'steer'(同轮可见,唯一可标记消费的路径)
|
|
475
|
-
* - 回合活跃但尚未 streaming(prompt 排队窗口,上限 20s)→ 等 streaming 开始后 steer
|
|
476
|
-
* - 回合被 host 工具阻塞(如 poll_messages 等待)/空闲/发送失败 → 返回 'deferred':
|
|
477
|
-
* 消息保持 pending,poll_messages 的 Mailbox 到信回调即时取走(毫秒级),或本回合
|
|
478
|
-
* 结束后由消费循环按 FIFO 起回合处理。
|
|
479
|
-
*/
|
|
480
|
-
async steer(text: string): Promise<'steer' | 'deferred'> {
|
|
481
|
-
// 从确定性触发横幅提取回执上下文(AgentRuntime.injectSteer 生成,格式固定):
|
|
482
|
-
// "[实时消息 from <id>]: ..." + "[系统触发器] 本消息要求回复(reply_to=<messageId>)。"
|
|
483
|
-
const banner = parseSteerBanner(text)
|
|
484
|
-
if (banner) {
|
|
485
|
-
this.toolState.replyContext = banner
|
|
486
|
-
}
|
|
487
|
-
const client = this.client
|
|
488
|
-
if (!client) return 'deferred'
|
|
489
|
-
try {
|
|
490
|
-
if (this.streaming) {
|
|
491
|
-
await client.send({ type: 'steer', message: text })
|
|
492
|
-
return 'steer'
|
|
493
|
-
}
|
|
494
|
-
if (this.turnActive) {
|
|
495
|
-
// prompt 排队窗口:等 streaming 开始(上限 20s);工具阻塞的回合等不到 → deferred
|
|
496
|
-
const deadline = Date.now() + 20_000
|
|
497
|
-
while (Date.now() < deadline && this.turnActive && !this.streaming) {
|
|
498
|
-
const { promise, resolve } = Promise.withResolvers()
|
|
499
|
-
setTimeout(resolve, 150)
|
|
500
|
-
await promise
|
|
501
|
-
}
|
|
502
|
-
if (this.streaming && this.turnActive) {
|
|
503
|
-
await client.send({ type: 'steer', message: text })
|
|
504
|
-
return 'steer'
|
|
505
|
-
}
|
|
506
|
-
return 'deferred'
|
|
507
|
-
}
|
|
508
|
-
// 空闲/回合已结束:deferred(消息 pending,消费循环 dequeue 即起回合)
|
|
509
|
-
return 'deferred'
|
|
510
|
-
}
|
|
511
|
-
catch (err) {
|
|
512
|
-
log.error(`[OmpRpcAgent:${this.selfAgentId}] steer 注入失败(消息保持 pending):`, err instanceof Error ? err.message : err)
|
|
513
|
-
return 'deferred'
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
async* run(request: AgentRunRequest, ctx: AgentRunContext): AsyncIterable<AgentEvent> {
|
|
518
|
-
const kind = request.message.metadata?.['x-aw-task-kind']
|
|
519
|
-
|
|
520
|
-
// worker: assign 消息 → 执行任务
|
|
521
|
-
if (kind === 'assign' && ctx.role === 'worker') {
|
|
522
|
-
yield* this.workerRun(request, ctx)
|
|
523
|
-
return
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// worker/lead: 同事点对点消息(含实时通信触发器)→ 按触发器语义处理并回复;
|
|
527
|
-
// 人类经 REST 注入的消息(from 空 + x-aw-from-label)同样进入应答流
|
|
528
|
-
if (!kind && (request.fromAgentId || request.message.metadata?.['x-aw-from-label'])) {
|
|
529
|
-
yield* this.peerMessageRun(request, ctx)
|
|
530
|
-
return
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// 其余消息(lead 的 assign/child-completed 等):no-op(调度由 supervise() 处理)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// ===== supervise() =====
|
|
537
|
-
|
|
538
|
-
/** supervise 单飞守卫:同一 client 不并发 LLM 回合(残留回合与下一 prompt 混流的根因) */
|
|
539
|
-
private supervising = false
|
|
540
|
-
|
|
541
|
-
async supervise(snapshot: SupervisionSnapshot, ctx: AgentRunContext, opts?: { signal?: AbortSignal }): Promise<SupervisionDecision[]> {
|
|
542
|
-
await this.ensureClient(ctx)
|
|
543
|
-
if (!this.client) return []
|
|
544
|
-
if (this.supervising) return [] // 上一轮 supervise 未收口:跳过本拍(节流即正确)
|
|
545
|
-
|
|
546
|
-
// 上下文门控(≥70% 先压缩再调度;回合间隙发起,失败放行)
|
|
547
|
-
await this.contextGate('supervise')
|
|
548
|
-
|
|
549
|
-
const prompt = supervisePrompt({
|
|
550
|
-
snapshot,
|
|
551
|
-
agentName: this.agentName,
|
|
552
|
-
channelId: this.channelId,
|
|
553
|
-
ctxPrefix: await this.contextPrefix(),
|
|
554
|
-
manual: this.systemManual(),
|
|
555
|
-
memory: ctx.memory,
|
|
556
|
-
})
|
|
557
|
-
// 150s 上界:supervise 持 lead.execLock 期间信箱消费停顿(更久会拖垮 worker 回执处理);
|
|
558
|
-
// supervise 是一次真实 LLM 回合:omp 冷启动(插件/MCP 加载 30~90s)+ 慢 provider
|
|
559
|
-
// 单步可能 >60s,过紧会把正常回合掐成 "Interrupted by user"。默认 150s,
|
|
560
|
-
// 仅拦真僵死(更紧的预算由调用方 config.superviseTimeoutMs 显式传入);真 abort 已实现。
|
|
561
|
-
const timeoutMs = this.config.superviseTimeoutMs ?? 150_000
|
|
562
|
-
|
|
563
|
-
return new Promise<SupervisionDecision[]>((resolve) => {
|
|
564
|
-
let assistantText = ''
|
|
565
|
-
let resolved = false
|
|
566
|
-
|
|
567
|
-
const finish = (decisions: SupervisionDecision[]) => {
|
|
568
|
-
if (resolved) return
|
|
569
|
-
resolved = true
|
|
570
|
-
this.supervising = false
|
|
571
|
-
unsub()
|
|
572
|
-
clearTimeout(timer)
|
|
573
|
-
signalUnsub?.()
|
|
574
|
-
resolve(decisions)
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const abortTurn = (): void => {
|
|
578
|
-
// 超时/外部取消:真正中止 omp 当前回合 —— 只 resolve 不 abort 会让残留回合
|
|
579
|
-
// 与下一个 prompt 在同一 client 混流(决策错位 + token 空烧)
|
|
580
|
-
log.warn(`[OmpRpcAgent:${this.selfAgentId}] supervise 超时(${timeoutMs}ms)→ abort 当前调度回合`)
|
|
581
|
-
void this.client?.send({ type: 'abort' }).catch(() => {})
|
|
582
|
-
finish([])
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const unsub = this.client!.onEvent((event) => {
|
|
586
|
-
if (event.type === 'message_update' && event.assistantMessageEvent?.type === 'text_delta') {
|
|
587
|
-
assistantText += event.assistantMessageEvent.delta ?? ''
|
|
588
|
-
}
|
|
589
|
-
if (event.type === 'agent_end' && event.isTerminal !== false) {
|
|
590
|
-
// 尝试从文本解析 JSON 决策(备用:如果 agent 没用 host tools 而是输出 JSON)
|
|
591
|
-
const parsed = extractJsonArray(assistantText)
|
|
592
|
-
if (parsed && parsed.length > 0) {
|
|
593
|
-
finish(parsed as SupervisionDecision[])
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
// agent 可能已通过 host tools 直接执行了调度,返回空(已执行)
|
|
597
|
-
finish([])
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
if (event.type === '__process_exit__' || event.type === '__error__') {
|
|
601
|
-
finish([])
|
|
602
|
-
}
|
|
603
|
-
})
|
|
604
|
-
|
|
605
|
-
const timer = setTimeout(abortTurn, timeoutMs)
|
|
606
|
-
|
|
607
|
-
// 外部取消(调度器 cancel 路径)传导:abort 当前 LLM 回合并立即收口
|
|
608
|
-
let signalUnsub: (() => void) | undefined
|
|
609
|
-
if (opts?.signal) {
|
|
610
|
-
if (opts.signal.aborted) {
|
|
611
|
-
unsub()
|
|
612
|
-
resolve([])
|
|
613
|
-
return
|
|
614
|
-
}
|
|
615
|
-
const onAbort = (): void => abortTurn()
|
|
616
|
-
opts.signal.addEventListener('abort', onAbort, { once: true })
|
|
617
|
-
signalUnsub = () => opts.signal?.removeEventListener('abort', onAbort)
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
this.supervising = true
|
|
621
|
-
this.client!.send({ type: 'prompt', message: prompt }).catch(() => finish([]))
|
|
622
|
-
})
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// ===== 内部:worker 执行 =====
|
|
626
|
-
|
|
627
|
-
private async* workerRun(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
|
|
628
|
-
const taskId = request.taskId
|
|
629
|
-
?? (request.message.metadata?.['x-aw-task-id'] as string | undefined)
|
|
630
|
-
if (!taskId) return
|
|
631
|
-
|
|
632
|
-
this.toolState.currentTaskId = taskId
|
|
633
|
-
|
|
634
|
-
try {
|
|
635
|
-
await this.ensureClient(ctx)
|
|
636
|
-
}
|
|
637
|
-
catch (err) {
|
|
638
|
-
yield {
|
|
639
|
-
kind: 'error',
|
|
640
|
-
error: {
|
|
641
|
-
code: 'OMP_SPAWN_FAILED',
|
|
642
|
-
message: `omp 子进程启动失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
643
|
-
},
|
|
644
|
-
}
|
|
645
|
-
return
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (!this.client) {
|
|
649
|
-
yield { kind: 'error', error: { code: 'OMP_NOT_READY', message: 'omp 客户端未就绪' } }
|
|
650
|
-
return
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// 构建 prompt
|
|
654
|
-
const taskText = request.message.parts.map((p) => {
|
|
655
|
-
if ('text' in p) return p.text
|
|
656
|
-
if ('data' in p) return JSON.stringify(p.data)
|
|
657
|
-
if ('url' in p) return p.url
|
|
658
|
-
if ('raw' in p) return p.raw
|
|
659
|
-
return ''
|
|
660
|
-
}).join('\n')
|
|
661
|
-
const prompt = workerPrompt({
|
|
662
|
-
agentName: this.agentName,
|
|
663
|
-
channelId: this.channelId,
|
|
664
|
-
taskId,
|
|
665
|
-
taskText,
|
|
666
|
-
memory: request.memory,
|
|
667
|
-
ctxPrefix: await this.contextPrefix(),
|
|
668
|
-
manual: this.systemManual(),
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
// 流式执行 + 事件映射
|
|
672
|
-
yield* this.promptAndStream(prompt, taskId, ctx.signal)
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* 点对点消息处理(实时通信驱动;lead 与 worker 通用)。
|
|
677
|
-
* 触发器语义:metadata['x-aw-require-reply']='true' → 必须经 send_message_to_agent
|
|
678
|
-
* 回给发送者:执行结果 + 对方所需内容,in_reply_to 关联原消息,并声明是否需再响应。
|
|
679
|
-
*/
|
|
680
|
-
private async* peerMessageRun(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
|
|
681
|
-
try {
|
|
682
|
-
await this.ensureClient(ctx)
|
|
683
|
-
}
|
|
684
|
-
catch (err) {
|
|
685
|
-
// spawn 失败必须显式报错:静默 return 会让 sawRunError=false → 消息被
|
|
686
|
-
// 直接 markConsumed,实时消息一次性丢失且无任何重试;产出 error 事件
|
|
687
|
-
// 走消息 requeue(≤2 次),与 workerRun 同口径
|
|
688
|
-
yield {
|
|
689
|
-
kind: 'error',
|
|
690
|
-
error: {
|
|
691
|
-
code: 'OMP_SPAWN_FAILED',
|
|
692
|
-
message: `omp 子进程启动失败(peer 消息未消费,将重试): ${err instanceof Error ? err.message : String(err)}`,
|
|
693
|
-
},
|
|
694
|
-
}
|
|
695
|
-
return
|
|
696
|
-
}
|
|
697
|
-
if (!this.client) {
|
|
698
|
-
yield { kind: 'error', error: { code: 'OMP_NOT_READY', message: 'omp 客户端未就绪(peer 消息未消费,将重试)' } }
|
|
699
|
-
return
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const msg = request.message
|
|
703
|
-
// 显示名:agent 同事用 id(名册可解析);人类发送者用 fromLabel;兜底 unknown
|
|
704
|
-
const fromId = request.fromAgentId
|
|
705
|
-
?? (typeof msg.metadata?.['x-aw-from-label'] === 'string' ? msg.metadata['x-aw-from-label'] : undefined)
|
|
706
|
-
?? 'unknown'
|
|
707
|
-
const msgText = msg.parts.map((p) => {
|
|
708
|
-
if ('text' in p) return p.text
|
|
709
|
-
if ('data' in p) return JSON.stringify(p.data)
|
|
710
|
-
if ('url' in p) return p.url
|
|
711
|
-
if ('raw' in p) return p.raw
|
|
712
|
-
return ''
|
|
713
|
-
}).join('\n')
|
|
714
|
-
const requireReply = msg.metadata?.['x-aw-require-reply'] === 'true'
|
|
715
|
-
const isReply = typeof msg.metadata?.['x-aw-in-reply-to'] === 'string'
|
|
716
|
-
// 跨 Channel 来信:发送方是其他 channel 的 lead,回执必须走 send_cross_channel_message
|
|
717
|
-
const crossChannel = msg.metadata?.['x-aw-cross-channel'] === 'true'
|
|
718
|
-
const fromChannel = typeof msg.metadata?.['x-aw-from-channel'] === 'string'
|
|
719
|
-
? String(msg.metadata['x-aw-from-channel'])
|
|
720
|
-
: ''
|
|
721
|
-
// 回执自动关联仅对真实 agent 发送者生效(人类无 agentId 可回投;
|
|
722
|
-
// 对人类的回复经事件流/时间线可见)
|
|
723
|
-
this.toolState.replyContext = requireReply && request.fromAgentId && !crossChannel
|
|
724
|
-
? { fromId: request.fromAgentId, messageId: msg.messageId }
|
|
725
|
-
: null
|
|
726
|
-
|
|
727
|
-
const prompt = peerPrompt({
|
|
728
|
-
agentName: this.agentName,
|
|
729
|
-
role: this.agentRole,
|
|
730
|
-
channelId: this.channelId,
|
|
731
|
-
ctxPrefix: await this.contextPrefix(),
|
|
732
|
-
manual: this.systemManual(),
|
|
733
|
-
memory: request.memory,
|
|
734
|
-
fromId,
|
|
735
|
-
messageId: msg.messageId,
|
|
736
|
-
requireReply,
|
|
737
|
-
isReply,
|
|
738
|
-
crossChannel,
|
|
739
|
-
fromChannel,
|
|
740
|
-
msgText,
|
|
741
|
-
})
|
|
742
|
-
|
|
743
|
-
yield* this.promptAndStream(prompt, undefined, ctx.signal)
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// ===== 内部:prompt 发送 + 事件流桥接 =====
|
|
747
|
-
|
|
748
|
-
private async* promptAndStream(
|
|
749
|
-
prompt: string,
|
|
750
|
-
taskId: string | undefined,
|
|
751
|
-
signal: AbortSignal,
|
|
752
|
-
): AsyncGenerator<AgentEvent, void, unknown> {
|
|
753
|
-
const client = this.client
|
|
754
|
-
if (!client) {
|
|
755
|
-
throw new Error('omp 客户端未就绪')
|
|
756
|
-
}
|
|
757
|
-
// 上下文门控(≥70% 时在回合间隙先压缩再发 prompt;等待有界,失败放行)
|
|
758
|
-
await this.contextGate('pre-prompt')
|
|
759
|
-
const queue: AgentEvent[] = []
|
|
760
|
-
let resolveWait: (() => void) | null = null
|
|
761
|
-
let isDone = false
|
|
762
|
-
// 回合生命周期:prompt 已发出 → turnActive;首条 message_update → streaming;message_end/turn_end → 结束
|
|
763
|
-
this.turnActive = true
|
|
764
|
-
this.streaming = false
|
|
765
|
-
this.turnText = ''
|
|
766
|
-
// 正文流式差额账本:contentIndex → 已流出长度。部分 provider(reasoning 模型/
|
|
767
|
-
// openai-completions)的 text 块不走 text_delta(只有换行),全文在 text_end.content
|
|
768
|
-
// 落定 —— 差额兜底把未流出部分补发为 delta,保证前端流式不缺正文。
|
|
769
|
-
const textSent = new Map<number, number>()
|
|
770
|
-
const unsubState = client.onEvent((event) => {
|
|
771
|
-
if (event.type === 'message_update') {
|
|
772
|
-
const ev = event.assistantMessageEvent as
|
|
773
|
-
| { type?: string, delta?: string, content?: string, contentIndex?: number }
|
|
774
|
-
| undefined
|
|
775
|
-
if (ev?.type === 'text_delta') {
|
|
776
|
-
this.streaming = true
|
|
777
|
-
const text = ev.delta ?? ''
|
|
778
|
-
const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
|
|
779
|
-
textSent.set(ci, (textSent.get(ci) ?? 0) + text.length)
|
|
780
|
-
this.turnText += text
|
|
781
|
-
// LLM 流式增量透出(AEP agent.delta 事件源):仅 worker/peer 转本走生成器
|
|
782
|
-
if (text) enqueueDelta(text)
|
|
783
|
-
}
|
|
784
|
-
else if (ev?.type === 'text_end' && typeof ev.content === 'string') {
|
|
785
|
-
const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
|
|
786
|
-
const sent = textSent.get(ci) ?? 0
|
|
787
|
-
if (ev.content.length > sent) {
|
|
788
|
-
const chunk = ev.content.slice(sent)
|
|
789
|
-
textSent.set(ci, ev.content.length)
|
|
790
|
-
this.streaming = true
|
|
791
|
-
this.turnText += chunk
|
|
792
|
-
enqueueDelta(chunk)
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (event.type === 'message_end' || event.type === 'turn_end') {
|
|
797
|
-
this.streaming = false
|
|
798
|
-
flushDelta()
|
|
799
|
-
}
|
|
800
|
-
if (event.type === 'agent_end' && event.isTerminal !== false) {
|
|
801
|
-
this.turnActive = false
|
|
802
|
-
this.streaming = false
|
|
803
|
-
flushDelta()
|
|
804
|
-
}
|
|
805
|
-
if (event.type === '__process_exit__' || event.type === '__error__') {
|
|
806
|
-
this.turnActive = false
|
|
807
|
-
this.streaming = false
|
|
808
|
-
}
|
|
809
|
-
})
|
|
810
|
-
|
|
811
|
-
const enqueue = (event: AgentEvent): void => {
|
|
812
|
-
queue.push(event)
|
|
813
|
-
if (event.kind === 'done' || event.kind === 'error') isDone = true
|
|
814
|
-
lastActivity = Date.now()
|
|
815
|
-
resolveWait?.()
|
|
816
|
-
resolveWait = null
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// 停滞看门狗:整轮无任何 omp 事件(挂死的 LLM 调用/子进程僵死)时中止回合,
|
|
820
|
-
// 否则消息永久卡 consuming、队友消息无限堆积。host 工具内阻塞
|
|
821
|
-
// (poll_messages 最长 180s)与慢 provider(实测单步可 >5min)都有
|
|
822
|
-
// tool/状态事件刷新计时;默认 600s 覆盖慢回合,只拦真僵死。
|
|
823
|
-
const idleTimeoutMs = this.config.promptTimeoutMs ?? 600_000
|
|
824
|
-
let lastActivity = Date.now()
|
|
825
|
-
let stallTimer: ReturnType<typeof setTimeout> | null = null
|
|
826
|
-
|
|
827
|
-
// LLM 流式增量缓冲:50ms 批量合并为一帧 delta(防高频 text_delta 洪泛 WS)
|
|
828
|
-
let deltaBuf = ''
|
|
829
|
-
let deltaTimer: ReturnType<typeof setTimeout> | null = null
|
|
830
|
-
const flushDelta = (): void => {
|
|
831
|
-
if (deltaTimer) {
|
|
832
|
-
clearTimeout(deltaTimer)
|
|
833
|
-
deltaTimer = null
|
|
834
|
-
}
|
|
835
|
-
if (!deltaBuf) return
|
|
836
|
-
const text = deltaBuf
|
|
837
|
-
deltaBuf = ''
|
|
838
|
-
enqueue({ kind: 'delta', delta: { text } })
|
|
839
|
-
}
|
|
840
|
-
const enqueueDelta = (text: string): void => {
|
|
841
|
-
deltaBuf += text
|
|
842
|
-
deltaTimer ??= setTimeout(() => {
|
|
843
|
-
deltaTimer = null
|
|
844
|
-
flushDelta()
|
|
845
|
-
}, 50)
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// 订阅 omp 事件流
|
|
849
|
-
const unsub = client.onEvent((event) => {
|
|
850
|
-
for (const mapped of this.mapOmpEvent(event, taskId)) {
|
|
851
|
-
enqueue(mapped)
|
|
852
|
-
}
|
|
853
|
-
})
|
|
854
|
-
|
|
855
|
-
// abort 传导
|
|
856
|
-
const onAbort = (): void => {
|
|
857
|
-
log.warn(`[OmpRpcAgent:${this.selfAgentId}] run 被 abort(signal)→ 中止 omp 回合,taskId=${taskId ?? '-'}`)
|
|
858
|
-
client.send({ type: 'abort' }).catch(() => {})
|
|
859
|
-
if (!isDone) {
|
|
860
|
-
enqueue({ kind: 'done', final: taskId ? { taskId } : undefined })
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
if (signal.aborted) {
|
|
864
|
-
onAbort()
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
signal.addEventListener('abort', onAbort, { once: true })
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// 发送 prompt
|
|
871
|
-
try {
|
|
872
|
-
await client.send({ type: 'prompt', message: prompt })
|
|
873
|
-
}
|
|
874
|
-
catch (err) {
|
|
875
|
-
unsub()
|
|
876
|
-
signal.removeEventListener('abort', onAbort)
|
|
877
|
-
// 僵死进程回收:prompt send 失败 = stdio 断裂或 60s 命令确认超时 —— 进程
|
|
878
|
-
// "活着但不干活"(alive-but-wedged)。若仅报错不清理,消息重试(≤2 次)会
|
|
879
|
-
// 复用同一僵死 stdio 每次空转 60s;必须杀掉并置空,下回合 ensureClient
|
|
880
|
-
// 全新重生(host tools/模型/终端 tap 随重建)。
|
|
881
|
-
log.warn(`[OmpRpcAgent:${this.selfAgentId}] prompt 失败 → 回收可疑僵死进程 pid=${client.pid}`)
|
|
882
|
-
this.killProcess()
|
|
883
|
-
this.client = null
|
|
884
|
-
this.hostToolsRegistered = false
|
|
885
|
-
yield {
|
|
886
|
-
kind: 'error',
|
|
887
|
-
error: {
|
|
888
|
-
code: 'PROMPT_FAILED',
|
|
889
|
-
message: `omp prompt 失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
890
|
-
},
|
|
891
|
-
}
|
|
892
|
-
return
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// 产出事件直到 done/error
|
|
896
|
-
try {
|
|
897
|
-
while (!isDone || queue.length > 0) {
|
|
898
|
-
if (queue.length === 0 && !isDone) {
|
|
899
|
-
const remaining = idleTimeoutMs - (Date.now() - lastActivity)
|
|
900
|
-
if (remaining <= 0) {
|
|
901
|
-
client.send({ type: 'abort' }).catch(() => {})
|
|
902
|
-
// 子进程大概率已僵死:击杀并清引用,下回合 ensureClient 全新重生
|
|
903
|
-
this.killProcess()
|
|
904
|
-
this.client = null
|
|
905
|
-
this.hostToolsRegistered = false
|
|
906
|
-
enqueue({
|
|
907
|
-
kind: 'error',
|
|
908
|
-
error: {
|
|
909
|
-
code: 'TURN_STALLED',
|
|
910
|
-
message: `回合停滞 ${Math.round(idleTimeoutMs / 1000)}s 无 omp 事件,已中止重置(消息按已处理落账,后续消息继续)`,
|
|
911
|
-
},
|
|
912
|
-
})
|
|
913
|
-
continue
|
|
914
|
-
}
|
|
915
|
-
await new Promise<void>((r) => {
|
|
916
|
-
resolveWait = r
|
|
917
|
-
stallTimer = setTimeout(() => {
|
|
918
|
-
resolveWait = null
|
|
919
|
-
r()
|
|
920
|
-
}, remaining + 100)
|
|
921
|
-
})
|
|
922
|
-
if (stallTimer) {
|
|
923
|
-
clearTimeout(stallTimer)
|
|
924
|
-
stallTimer = null
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
while (queue.length > 0) {
|
|
928
|
-
yield queue.shift()!
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
finally {
|
|
933
|
-
if (stallTimer) clearTimeout(stallTimer)
|
|
934
|
-
// 事件监听必须全部解除:本 client 跨回合长期复用,残留的 mapOmpEvent
|
|
935
|
-
// 闭包会在后续每一帧继续执行并把事件 push 进孤儿队列(内存/CPU 随
|
|
936
|
-
// 回合数线性劣化,直接违背进程常驻的稳定性目标)
|
|
937
|
-
unsubState()
|
|
938
|
-
unsub()
|
|
939
|
-
signal.removeEventListener('abort', onAbort)
|
|
940
|
-
this.turnActive = false
|
|
941
|
-
this.streaming = false
|
|
942
|
-
this.toolState.currentTaskId = null
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// ===== 内部:omp 事件 → AgentEvent 映射 =====
|
|
947
|
-
|
|
948
|
-
private mapOmpEvent(event: AgentSessionEvent, taskId: string | undefined): AgentEvent[] {
|
|
949
|
-
switch (event.type) {
|
|
950
|
-
case 'agent_start':
|
|
951
|
-
return [{
|
|
952
|
-
kind: 'status',
|
|
953
|
-
status: { state: 'WORKING', timestamp: new Date().toISOString() },
|
|
954
|
-
}]
|
|
955
|
-
case 'message_end': {
|
|
956
|
-
// 消息完成:如果有 message 内容,产出为 status 事件(任务历史追踪)
|
|
957
|
-
const msg = event.message
|
|
958
|
-
if (msg && Array.isArray(msg.content)) {
|
|
959
|
-
const text = (msg.content as Array<{ type?: string, text?: string }>)
|
|
960
|
-
.filter(c => c.type === 'text')
|
|
961
|
-
.map(c => c.text ?? '')
|
|
962
|
-
.join('')
|
|
963
|
-
if (text) {
|
|
964
|
-
const a2aMsg: A2AMessage = {
|
|
965
|
-
messageId: randomUUID(),
|
|
966
|
-
contextId: this.channelId,
|
|
967
|
-
role: 'ROLE_AGENT',
|
|
968
|
-
parts: [{ text }],
|
|
969
|
-
}
|
|
970
|
-
return [{
|
|
971
|
-
kind: 'status',
|
|
972
|
-
status: { state: 'WORKING', message: a2aMsg, timestamp: new Date().toISOString() },
|
|
973
|
-
}]
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
return []
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
case 'tool_execution_start': {
|
|
980
|
-
const toolName = event.toolName ?? 'tool'
|
|
981
|
-
return [{
|
|
982
|
-
kind: 'status',
|
|
983
|
-
status: {
|
|
984
|
-
state: 'WORKING',
|
|
985
|
-
message: {
|
|
986
|
-
messageId: randomUUID(),
|
|
987
|
-
contextId: this.channelId,
|
|
988
|
-
role: 'ROLE_AGENT',
|
|
989
|
-
parts: [{ text: `🔧 ${toolName}${toolArgsPreview(event.args)}` }],
|
|
990
|
-
},
|
|
991
|
-
timestamp: new Date().toISOString(),
|
|
992
|
-
},
|
|
993
|
-
}]
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
case 'agent_end': {
|
|
997
|
-
if (event.isTerminal === false) return []
|
|
998
|
-
const events: AgentEvent[] = []
|
|
999
|
-
// 提取最终 assistant 文本作为 artifact
|
|
1000
|
-
const messages = event.messages ?? []
|
|
1001
|
-
const assistantText = messages
|
|
1002
|
-
.filter(m => m.role === 'assistant')
|
|
1003
|
-
.flatMap(m => m.content.filter(c => c.type === 'text').map(c => c.text ?? ''))
|
|
1004
|
-
.join('')
|
|
1005
|
-
// 回合内的 provider/API 失败(限额 429、鉴权、上游 5xx 等):omp 把错误挂在最后一条
|
|
1006
|
-
// assistant 消息上(stopReason='error' + errorStatus/errorMessage)而非 __error__ 帧。
|
|
1007
|
-
// 不映射出来,上层只能看到「回合结束无产出」,真实原因彻底丢失(且照常重试到耗尽)。
|
|
1008
|
-
const failure = [...messages].reverse().find((m) => {
|
|
1009
|
-
const mm = m as { role?: string, stopReason?: string, errorMessage?: string }
|
|
1010
|
-
return mm.role === 'assistant' && (mm.stopReason === 'error' || typeof mm.errorMessage === 'string')
|
|
1011
|
-
}) as { errorStatus?: number, errorMessage?: string } | undefined
|
|
1012
|
-
if (failure) {
|
|
1013
|
-
const code = failure.errorStatus != null ? `OMP_LLM_${failure.errorStatus}` : 'OMP_LLM_ERROR'
|
|
1014
|
-
const detail = failure.errorMessage ?? 'harness 回合以错误结束(无错误详情)'
|
|
1015
|
-
log.error(`[OmpRpcAgent:${this.selfAgentId}] harness 回合失败 ${code}: ${detail}`)
|
|
1016
|
-
// error 事件即回合终点(队列侧据此收口),不再追加 done
|
|
1017
|
-
return [{ kind: 'error', error: { code, message: detail } }]
|
|
1018
|
-
}
|
|
1019
|
-
if (assistantText) {
|
|
1020
|
-
events.push({
|
|
1021
|
-
kind: 'artifact',
|
|
1022
|
-
artifact: {
|
|
1023
|
-
artifactId: randomUUID(),
|
|
1024
|
-
name: 'output',
|
|
1025
|
-
parts: [{ text: assistantText }],
|
|
1026
|
-
},
|
|
1027
|
-
lastChunk: true,
|
|
1028
|
-
totalChunks: 1,
|
|
1029
|
-
})
|
|
1030
|
-
}
|
|
1031
|
-
events.push({ kind: 'done', final: taskId ? { taskId } : undefined })
|
|
1032
|
-
return events
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
case '__process_exit__':
|
|
1036
|
-
return [{
|
|
1037
|
-
kind: 'error',
|
|
1038
|
-
error: { code: 'OMP_PROCESS_EXIT', message: 'omp 子进程意外退出' },
|
|
1039
|
-
}]
|
|
1040
|
-
|
|
1041
|
-
// ===== 上下文治理事件(回合中到达的原生阈值/overflow 压缩;不污染事件流)=====
|
|
1042
|
-
case 'compaction_start':
|
|
1043
|
-
this.compacting = true
|
|
1044
|
-
return []
|
|
1045
|
-
|
|
1046
|
-
case 'compaction_end': {
|
|
1047
|
-
this.compacting = false
|
|
1048
|
-
const result = (event as { result?: CompactionResult }).result
|
|
1049
|
-
if (result?.summary) {
|
|
1050
|
-
void this.harvestCompaction(
|
|
1051
|
-
result.summary,
|
|
1052
|
-
result.tokensBefore,
|
|
1053
|
-
result.tokensAfter ?? result.estimatedTokensAfter,
|
|
1054
|
-
(event as { reason?: string }).reason,
|
|
1055
|
-
)
|
|
1056
|
-
}
|
|
1057
|
-
return []
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// 回合完全落定(无自动重试/压缩重试/排队续跑):后续压缩检查由 AgentRuntime 驱动
|
|
1061
|
-
case 'agent_settled':
|
|
1062
|
-
return []
|
|
1063
|
-
|
|
1064
|
-
case '__error__':
|
|
1065
|
-
return [{
|
|
1066
|
-
kind: 'error',
|
|
1067
|
-
error: { code: 'OMP_ERROR', message: (event as { error?: string }).error ?? 'omp 未知错误' },
|
|
1068
|
-
}]
|
|
1069
|
-
|
|
1070
|
-
default:
|
|
1071
|
-
return []
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
// ===== 内部:omp 客户端管理 =====
|
|
1076
|
-
|
|
1077
|
-
private async ensureClient(ctx: AgentRunContext): Promise<void> {
|
|
1078
|
-
if (!this.workspace) {
|
|
1079
|
-
this.workspace = ctx.workspace
|
|
1080
|
-
}
|
|
1081
|
-
if (!this.agentInfo) {
|
|
1082
|
-
// init() 可能未调用;从 ctx 推断
|
|
1083
|
-
this.channelId = ctx.channelId
|
|
1084
|
-
this.agentRole = ctx.role
|
|
1085
|
-
this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
if (!this.client || !this.client.alive) {
|
|
1089
|
-
// 旧客户端已退出(exit 事件/OS 存活校准 reconcile 触发)——必须丢弃并重生,
|
|
1090
|
-
// 否则后续回合会一直对着死 stdio 报 PROMPT_FAILED 烧重试配额
|
|
1091
|
-
this.client = null
|
|
1092
|
-
this.hostToolsRegistered = false
|
|
1093
|
-
const command = this.config.command ?? 'omp'
|
|
1094
|
-
const client = new OmpRpcClient({
|
|
1095
|
-
command,
|
|
1096
|
-
mode: this.rpcMode,
|
|
1097
|
-
args: [...(this.config.thinkingLevel ? ['--thinking', this.config.thinkingLevel] : [])],
|
|
1098
|
-
args: this.config.args,
|
|
1099
|
-
cwd: this.config.cwd ?? process.cwd(),
|
|
1100
|
-
// 进程退出 → 注册表标记(供运行时资源监控);pid=-1 表示无法取得,忽略
|
|
1101
|
-
onExit: (pid, code) => {
|
|
1102
|
-
if (pid > 0) {
|
|
1103
|
-
markHarnessProcessExit(pid, code)
|
|
1104
|
-
markTerminalSessionExit(pid, code)
|
|
1105
|
-
}
|
|
1106
|
-
},
|
|
1107
|
-
})
|
|
1108
|
-
await client.start()
|
|
1109
|
-
|
|
1110
|
-
// 登记 + 绑定 agent 身份(harness 进程监控的事实源)
|
|
1111
|
-
const pid = client.pid
|
|
1112
|
-
if (pid) {
|
|
1113
|
-
registerHarnessProcess(pid, {
|
|
1114
|
-
harness: 'omp',
|
|
1115
|
-
command,
|
|
1116
|
-
args: ['--mode', this.rpcMode, ...(this.config.args ?? [])],
|
|
1117
|
-
})
|
|
1118
|
-
bindHarnessProcess(pid, {
|
|
1119
|
-
agentId: this.selfAgentId,
|
|
1120
|
-
channelId: this.channelId,
|
|
1121
|
-
name: this.agentName,
|
|
1122
|
-
role: this.agentRole,
|
|
1123
|
-
})
|
|
1124
|
-
// 终端镜像 tap:全部 RPC 帧 → /monitor 终端(实时 TUI 渲染 + HITL)
|
|
1125
|
-
attachTerminalTap(client, {
|
|
1126
|
-
pid,
|
|
1127
|
-
harness: 'omp',
|
|
1128
|
-
agentId: this.selfAgentId,
|
|
1129
|
-
channelId: this.channelId,
|
|
1130
|
-
name: this.agentName,
|
|
1131
|
-
role: this.agentRole,
|
|
1132
|
-
})
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// 设置模型(如果配置了 provider/model 任一;omp 以当前已设值为缺省补全,
|
|
1136
|
-
// 只配其一时也显式发送,避免 respawn 后静默回落默认模型)
|
|
1137
|
-
if (this.config.provider || this.config.model) {
|
|
1138
|
-
try {
|
|
1139
|
-
// 未配置一侧保持 undefined(JSON 序列化丢键)→ omp 以当前已设值补全
|
|
1140
|
-
await client.send({
|
|
1141
|
-
type: 'set_model',
|
|
1142
|
-
provider: this.config.provider as string,
|
|
1143
|
-
modelId: this.config.model as string,
|
|
1144
|
-
})
|
|
1145
|
-
}
|
|
1146
|
-
catch {
|
|
1147
|
-
// 模型设置失败不致命(用 omp 默认模型)
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// 注册 host tools(按角色差异化:lead 全量,worker 剔除调度/团队管理专属工具)
|
|
1152
|
-
client.onHostToolCall(req => this.handleHostTool(req))
|
|
1153
|
-
await client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole) })
|
|
1154
|
-
|
|
1155
|
-
// 上下文治理探测(feature-detect 一次;失败退化被动 usage 跟踪)+ 原生压缩兜底保持开启
|
|
1156
|
-
void this.probeContext(client).catch(() => {})
|
|
1157
|
-
void client.send({ type: 'set_auto_compaction', enabled: true }).catch(() => {})
|
|
1158
|
-
|
|
1159
|
-
this.client = client
|
|
1160
|
-
this.hostToolsRegistered = true
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
/** 上下文状态探测:get_state 取 contextWindow/sessionId(一次;失败仅损失 percent 精度) */
|
|
1165
|
-
private async probeContext(client: OmpRpcClient): Promise<void> {
|
|
1166
|
-
try {
|
|
1167
|
-
const resp = await client.send({ type: 'get_state' })
|
|
1168
|
-
const data = (resp.data ?? {}) as Record<string, unknown>
|
|
1169
|
-
const model = data.model as { contextWindow?: number } | undefined
|
|
1170
|
-
if (typeof model?.contextWindow === 'number' && model.contextWindow > 0) {
|
|
1171
|
-
this.contextWindow = model.contextWindow
|
|
1172
|
-
client.setContextWindow(model.contextWindow)
|
|
1173
|
-
}
|
|
1174
|
-
if (typeof data.sessionId === 'string' && data.sessionId) this.sessionId = data.sessionId
|
|
1175
|
-
}
|
|
1176
|
-
catch { /* get_state 不可用:仅被动 usage */ }
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* OmpRpcAgentImpl — omp harness 的完整 AgentInterface 实现。
|
|
3
|
+
*
|
|
4
|
+
* 通过 OmpRpcClient 驱动真实 omp 子进程(--mode rpc),将 omp 的原生 agent 能力
|
|
5
|
+
* (LLM 推理 + 内置工具 read/write/edit/bash/grep/glob 等)接入 Workshop 的
|
|
6
|
+
* Channel 编排体系。
|
|
7
|
+
*
|
|
8
|
+
* 核心设计:
|
|
9
|
+
* - 每个 Agent 实例持有一个 omp 子进程(lazy spawn,跨消息复用)
|
|
10
|
+
* - AgentWorkspace 方法经 host-tool-bridge 注册为 omp host tools,agent 原生调用
|
|
11
|
+
* report_progress / complete_task / dispatch_task / send_message 等,无需文本解析
|
|
12
|
+
* - omp AgentSessionEvent → AgentEvent 五变体映射
|
|
13
|
+
* - worker run():接收 assign → prompt omp → agent 用原生工具作业 + host tools 管理任务生命周期
|
|
14
|
+
* - lead supervise():格式化快照 → prompt omp → agent 直接用 host tools 执行调度决策 → 返回 []
|
|
15
|
+
* - dispose():杀子进程
|
|
16
|
+
*
|
|
17
|
+
* prompt 组装与工具分发已抽至共享层(prompt-builder / host-tool-bridge),
|
|
18
|
+
* 与 codex/dsh/opencode impl 全引擎一致。协议权威:omp://rpc.md。
|
|
19
|
+
*/
|
|
20
|
+
import { createLogger } from '../logger'
|
|
21
|
+
import { randomUUID } from 'node:crypto'
|
|
22
|
+
import type {
|
|
23
|
+
AgentEvent,
|
|
24
|
+
AgentInterface,
|
|
25
|
+
AgentInfo,
|
|
26
|
+
AgentRunContext,
|
|
27
|
+
AgentRunRequest,
|
|
28
|
+
SupervisionDecision,
|
|
29
|
+
SupervisionSnapshot,
|
|
30
|
+
} from './agent-interface'
|
|
31
|
+
import type { A2AMessage } from '../types/a2a'
|
|
32
|
+
import type { AgentContextStats } from '../types/task'
|
|
33
|
+
import {
|
|
34
|
+
OmpRpcClient,
|
|
35
|
+
type AgentSessionEvent,
|
|
36
|
+
type CompactionResult,
|
|
37
|
+
type HostToolCallRequest,
|
|
38
|
+
} from './adapters/omp-rpc-client'
|
|
39
|
+
import {
|
|
40
|
+
registerHarnessProcess,
|
|
41
|
+
bindHarnessProcess,
|
|
42
|
+
markHarnessProcessExit,
|
|
43
|
+
killHarnessProcess,
|
|
44
|
+
} from './harness-process'
|
|
45
|
+
import {
|
|
46
|
+
attachTerminalTap,
|
|
47
|
+
markTerminalSessionExit,
|
|
48
|
+
} from './harness-terminal'
|
|
49
|
+
import {
|
|
50
|
+
createSessionState,
|
|
51
|
+
dispatchHostTool,
|
|
52
|
+
hostToolsForRole,
|
|
53
|
+
} from './host-tool-bridge'
|
|
54
|
+
import {
|
|
55
|
+
contextPrefix as buildContextPrefix,
|
|
56
|
+
createRosterCache,
|
|
57
|
+
extractJsonArray,
|
|
58
|
+
parseSteerBanner,
|
|
59
|
+
peerPrompt,
|
|
60
|
+
supervisePrompt,
|
|
61
|
+
systemManual,
|
|
62
|
+
toolArgsPreview,
|
|
63
|
+
workerPrompt,
|
|
64
|
+
} from './prompt-builder'
|
|
65
|
+
import { renderPrompt } from '../prompts/loader'
|
|
66
|
+
import { attachOmpPluginBridge, onPluginToolsChange } from './plugin-tools'
|
|
67
|
+
import { ompSettings } from '../settings'
|
|
68
|
+
|
|
69
|
+
const log = createLogger('workshop.omp')
|
|
70
|
+
|
|
71
|
+
// 兼容再导出(脚本/测试历史导入面;实现在 host-tool-bridge)
|
|
72
|
+
export { HOST_TOOLS, hostToolsForRole } from './host-tool-bridge'
|
|
73
|
+
|
|
74
|
+
// ===== 配置 =====
|
|
75
|
+
|
|
76
|
+
export interface OmpAgentConfig {
|
|
77
|
+
/** omp 可执行文件路径(默认 'omp',从 PATH 查找) */
|
|
78
|
+
command?: string
|
|
79
|
+
/** 额外 CLI 参数 */
|
|
80
|
+
args?: string[]
|
|
81
|
+
/** omp 工作目录(默认 process.cwd()) */
|
|
82
|
+
cwd?: string
|
|
83
|
+
/** 模型 provider(如 'anthropic'/'openai'/'zhipu');省略则用 omp 默认 */
|
|
84
|
+
provider?: string
|
|
85
|
+
/** 模型 ID;省略则用 omp 默认 */
|
|
86
|
+
model?: string
|
|
87
|
+
/** thinking level: off/minimal/low/medium/high/xhigh/max */
|
|
88
|
+
thinkingLevel?: string
|
|
89
|
+
/** 自定义系统 prompt 前缀(拼接到 agent 系统指令之前) */
|
|
90
|
+
systemPromptPrefix?: string
|
|
91
|
+
/** 限制 omp 仅使用指定内置工具(null = 全部) */
|
|
92
|
+
toolNames?: string[]
|
|
93
|
+
/** 每轮停滞超时(ms,默认 300000):整轮无任何 omp 事件才中止;工具内阻塞(poll_messages ≤180s)有 tool 事件刷新计时 */
|
|
94
|
+
promptTimeoutMs?: number
|
|
95
|
+
/** supervise 轮超时(ms,默认 150000) */
|
|
96
|
+
superviseTimeoutMs?: number
|
|
97
|
+
/**
|
|
98
|
+
* omp 输出模式:'rpc-ui'(默认)= RPC 协议 + UI 上下文 —— ask 工具与
|
|
99
|
+
* extension_ui_request 对话框可用(监控终端 HITL 通道,强制 noPty);
|
|
100
|
+
* 'rpc' = 纯协议(无 UI,ask 不可用)。
|
|
101
|
+
*/
|
|
102
|
+
rpcMode?: 'rpc' | 'rpc-ui'
|
|
103
|
+
/** channel 级作业场景 prompt(manager 装配时注入;用户场景规范,全员共享) */
|
|
104
|
+
scenarioPrompt?: string
|
|
105
|
+
/** agent 身份(由 factory 从 AgentInfo 注入) */
|
|
106
|
+
agentId?: string
|
|
107
|
+
name?: string
|
|
108
|
+
role?: 'lead' | 'worker'
|
|
109
|
+
channelId?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 插件桥接管(host.mjs ctx.omp 排队注册的工具在此回放;幂等)
|
|
113
|
+
attachOmpPluginBridge()
|
|
114
|
+
|
|
115
|
+
/** 在跑 agent 实例表(工具注册表变更 → 热重发 set_host_tools,运行时注入无需重spawn) */
|
|
116
|
+
const gLive = globalThis as typeof globalThis & { __ompLiveAgents?: Set<OmpRpcAgentImpl> }
|
|
117
|
+
function liveAgents(): Set<OmpRpcAgentImpl> {
|
|
118
|
+
return gLive.__ompLiveAgents ??= new Set()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 工具注册表变更 → 全部在跑会话热更新工具面
|
|
122
|
+
onPluginToolsChange(() => {
|
|
123
|
+
for (const impl of liveAgents()) {
|
|
124
|
+
try {
|
|
125
|
+
impl.refreshPluginTools()
|
|
126
|
+
}
|
|
127
|
+
catch { /* 单实例失败不影响其他 */ }
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// ===== OmpRpcAgentImpl =====
|
|
132
|
+
|
|
133
|
+
export class OmpRpcAgentImpl implements AgentInterface {
|
|
134
|
+
private readonly config: OmpAgentConfig
|
|
135
|
+
private client: OmpRpcClient | null = null
|
|
136
|
+
private workspace: AgentRunContext['workspace'] | null = null
|
|
137
|
+
private agentInfo: AgentInfo | null = null
|
|
138
|
+
private hostToolsRegistered = false
|
|
139
|
+
/** host tool 会话态(当前任务 + 待回执上下文;与共享桥共用) */
|
|
140
|
+
private readonly toolState = createSessionState()
|
|
141
|
+
/** 工具桥上下文(分发唯一入口) */
|
|
142
|
+
private readonly bridgeCtx = {
|
|
143
|
+
identity: { agentId: '', channelId: '', role: 'worker' as 'lead' | 'worker', name: 'agent' },
|
|
144
|
+
state: this.toolState,
|
|
145
|
+
getWorkspace: () => this.workspace,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 会话回合状态(steer 可靠注入的依据):
|
|
150
|
+
* omp 的 steer 仅在回合 streaming 中生效——prompt 已入列但尚未开始输出时,
|
|
151
|
+
* steer 会"成功"返回但被静默丢弃。因此注入方轮询直到回合输出开始(streamingStarted)
|
|
152
|
+
* 才发送;若回合在等待期间结束,消息保持 pending 由消费循环处理。
|
|
153
|
+
*/
|
|
154
|
+
private streaming = false
|
|
155
|
+
private turnActive = false
|
|
156
|
+
/** 当前回合产生的 assistant 文本(供诊断) */
|
|
157
|
+
private turnText = ''
|
|
158
|
+
/** agent 身份信息(factory 注入;无需等待 init()) */
|
|
159
|
+
private selfAgentId = ''
|
|
160
|
+
private agentName = 'agent'
|
|
161
|
+
private agentRole: 'lead' | 'worker' = 'worker'
|
|
162
|
+
private channelId = ''
|
|
163
|
+
/** 团队名册缓存(共享 prompt-builder) */
|
|
164
|
+
private roster = createRosterCache({
|
|
165
|
+
selfAgentId: '',
|
|
166
|
+
listAgents: () => this.workspace!.listAgents(),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// ===== 上下文治理(70% 无中断压缩环)=====
|
|
170
|
+
/** 压缩进行中(平台 gate 与 omp 原生压缩共用互斥位) */
|
|
171
|
+
private compacting = false
|
|
172
|
+
private lastCompactAt = 0
|
|
173
|
+
/** omp 不支持 compact 命令(旧版)→ 永久停用平台压缩;原生 auto-compaction 兜底,harvest 照常 */
|
|
174
|
+
private compactLegacy = false
|
|
175
|
+
/** 模型上下文窗口(get_state/get_session_stats 探测;null = 未知,percent 不可算) */
|
|
176
|
+
private contextWindow: number | null = null
|
|
177
|
+
private sessionId: string | null = null
|
|
178
|
+
/** harvest 双路去重(compact 响应与 compaction_end 事件可能双达) */
|
|
179
|
+
private lastHarvestKey = ''
|
|
180
|
+
private lastHarvestAt = 0
|
|
181
|
+
|
|
182
|
+
/** 总开关(omp.compact_enabled;env AW_OMP_COMPACT_ENABLED=0 显式关闭兼容) */
|
|
183
|
+
private compactEnabled(): boolean {
|
|
184
|
+
return ompSettings().compact_enabled
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private compactThreshold(): number {
|
|
188
|
+
return ompSettings().compact_threshold
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private compactMinIntervalMs(): number {
|
|
192
|
+
return ompSettings().compact_min_interval_ms
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private compactWaitMs(): number {
|
|
196
|
+
return ompSettings().compact_wait_ms
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 被动上下文用量快照(无探测 RPC;getStatus 透出用) */
|
|
200
|
+
getContextStats(): AgentContextStats | null {
|
|
201
|
+
const usage = this.client?.getContextUsage() ?? null
|
|
202
|
+
const window = this.contextWindow ?? usage?.contextWindow ?? null
|
|
203
|
+
if (!usage && window === null) return null
|
|
204
|
+
const usedTokens = usage?.tokens ?? 0
|
|
205
|
+
return {
|
|
206
|
+
usedTokens,
|
|
207
|
+
contextWindow: window,
|
|
208
|
+
percent: window && window > 0 ? Math.min(1, usedTokens / window) : null,
|
|
209
|
+
compacting: this.compacting,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 上下文用量探测:get_session_stats 权威值;不可用退化为被动 usage + 已探测窗口。
|
|
215
|
+
* percent 归一化到 0-1(omp 可能返回 0-100)。
|
|
216
|
+
*/
|
|
217
|
+
private async probeUsage(client: OmpRpcClient): Promise<{ tokens: number, contextWindow: number | null, percent: number | null } | null> {
|
|
218
|
+
try {
|
|
219
|
+
const resp = await client.send({ type: 'get_session_stats' })
|
|
220
|
+
const cu = ((resp.data ?? {}) as Record<string, unknown>).contextUsage as
|
|
221
|
+
| { tokens?: number, contextWindow?: number, percent?: number }
|
|
222
|
+
| undefined
|
|
223
|
+
if (cu && typeof cu.tokens === 'number' && cu.tokens > 0) {
|
|
224
|
+
if (typeof cu.contextWindow === 'number' && cu.contextWindow > 0) {
|
|
225
|
+
this.contextWindow = cu.contextWindow
|
|
226
|
+
client.setContextWindow(cu.contextWindow)
|
|
227
|
+
}
|
|
228
|
+
const window = this.contextWindow ?? cu.contextWindow ?? null
|
|
229
|
+
const rawPercent = typeof cu.percent === 'number' ? (cu.percent > 1 ? cu.percent / 100 : cu.percent) : null
|
|
230
|
+
const percent = rawPercent ?? (window && window > 0 ? Math.min(1, cu.tokens / window) : null)
|
|
231
|
+
return { tokens: cu.tokens, contextWindow: window, percent }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch { /* get_session_stats 不可用 → 被动跟踪 */ }
|
|
235
|
+
return client.getContextUsage()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 上下文治理门控:三条 prompt 路径(worker/peer/supervise)在回合间隙统一调用;
|
|
240
|
+
* post-settle 经 onTurnSettled 复用同一守卫。任何失败路径都放行——压缩晚一轮
|
|
241
|
+
* 永远好过中断作业。三重防线:仅回合间隙发起 → get_state 复查 isStreaming/
|
|
242
|
+
* isCompacting → compacting 互斥位 + 最小间隔防振荡。
|
|
243
|
+
*/
|
|
244
|
+
private async contextGate(reason: string): Promise<void> {
|
|
245
|
+
if (!this.compactEnabled() || this.compactLegacy || this.compacting) return
|
|
246
|
+
const client = this.client
|
|
247
|
+
if (!client || !client.alive) return
|
|
248
|
+
try {
|
|
249
|
+
const usage = await this.probeUsage(client)
|
|
250
|
+
if (!usage || usage.percent === null || usage.percent < this.compactThreshold()) return
|
|
251
|
+
if (Date.now() - this.lastCompactAt < this.compactMinIntervalMs()) return
|
|
252
|
+
// 回合间隙硬校验:流式中/压缩中绝不发起(不中断执行的硬约束)
|
|
253
|
+
const st = await client.send({ type: 'get_state' })
|
|
254
|
+
const s = (st.data ?? {}) as Record<string, unknown>
|
|
255
|
+
if (s.isStreaming === true || s.isCompacting === true) return
|
|
256
|
+
if (typeof s.sessionId === 'string' && s.sessionId) this.sessionId = s.sessionId
|
|
257
|
+
this.compacting = true
|
|
258
|
+
this.lastCompactAt = Date.now()
|
|
259
|
+
try {
|
|
260
|
+
const result = await this.runCompaction(client)
|
|
261
|
+
if (result?.summary) {
|
|
262
|
+
await this.harvestCompaction(
|
|
263
|
+
result.summary,
|
|
264
|
+
result.tokensBefore,
|
|
265
|
+
result.tokensAfter ?? result.estimatedTokensAfter,
|
|
266
|
+
reason,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
this.compacting = false
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
276
|
+
if (/compact/i.test(msg) && /unknown|unsupported|invalid|无效|未知|不支持/i.test(msg)) this.compactLegacy = true
|
|
277
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] contextGate(${reason}) 失败,放行: ${msg}`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 发起 compact 并等待 compaction_end(响应内含 result 则立即收口;
|
|
283
|
+
* 超时放行——压缩在 harness 内后台继续,isCompacting 防后续双发)。
|
|
284
|
+
*/
|
|
285
|
+
private runCompaction(client: OmpRpcClient): Promise<CompactionResult | null> {
|
|
286
|
+
return new Promise<CompactionResult | null>((resolve) => {
|
|
287
|
+
let settled = false
|
|
288
|
+
const finish = (r: CompactionResult | null): void => {
|
|
289
|
+
if (settled) return
|
|
290
|
+
settled = true
|
|
291
|
+
off()
|
|
292
|
+
clearTimeout(timer)
|
|
293
|
+
resolve(r)
|
|
294
|
+
}
|
|
295
|
+
const off = client.onEvent((event) => {
|
|
296
|
+
if (event.type === 'compaction_end') {
|
|
297
|
+
finish((event as { result?: CompactionResult }).result ?? null)
|
|
298
|
+
}
|
|
299
|
+
else if (event.type === '__process_exit__') {
|
|
300
|
+
finish(null)
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
const timer = setTimeout(() => {
|
|
304
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 等待超时(${this.compactWaitMs()}ms)→ 放行(压缩后台继续)`)
|
|
305
|
+
finish(null)
|
|
306
|
+
}, this.compactWaitMs())
|
|
307
|
+
void client.send({ type: 'compact', customInstructions: this.compactionHints() })
|
|
308
|
+
.then((resp) => {
|
|
309
|
+
const result = (resp.data as { result?: CompactionResult } | undefined)?.result
|
|
310
|
+
if (result?.summary) finish(result)
|
|
311
|
+
// 响应无 result:等 compaction_end 事件(协议权威语义)
|
|
312
|
+
})
|
|
313
|
+
.catch((err) => {
|
|
314
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
315
|
+
if (/compact/i.test(msg)) this.compactLegacy = true
|
|
316
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 发起失败: ${msg}`)
|
|
317
|
+
finish(null)
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** 压缩摘要指令(外置 prompts;缺失时省略,omp 用默认策略) */
|
|
323
|
+
private compactionHints(): string | undefined {
|
|
324
|
+
try {
|
|
325
|
+
return renderPrompt('compaction-hints')
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return undefined
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 压缩摘要 harvest(三路统一:平台主动 compact / omp 阈值自动 / overflow):
|
|
334
|
+
* 经 workspace.recordSessionMemory 落库为本人 episodic-session 记忆。
|
|
335
|
+
* 双路去重:同摘要 60s 内只入库一次。
|
|
336
|
+
*/
|
|
337
|
+
private async harvestCompaction(summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string): Promise<void> {
|
|
338
|
+
const key = summary.slice(0, 120)
|
|
339
|
+
if (key === this.lastHarvestKey && Date.now() - this.lastHarvestAt < 60_000) return
|
|
340
|
+
this.lastHarvestKey = key
|
|
341
|
+
this.lastHarvestAt = Date.now()
|
|
342
|
+
try {
|
|
343
|
+
await this.workspace?.recordSessionMemory?.({ summary, tokensBefore, tokensAfter, reason })
|
|
344
|
+
log.info(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要已入记忆(${summary.length} 字,reason=${reason ?? 'auto'})`)
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要入库失败(不影响会话):`, err instanceof Error ? err.message : err)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** 回合落定钩子(AgentRuntime 在信箱空时调用):后台压缩检查,自守卫不抛错 */
|
|
352
|
+
async onTurnSettled(): Promise<void> {
|
|
353
|
+
await this.contextGate('post-settle')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
constructor(config: Record<string, unknown> = {}) {
|
|
357
|
+
this.config = config as OmpAgentConfig
|
|
358
|
+
// effort 统一入口(channel 默认 LLM 注入 config.effort)→ omp thinking 级别
|
|
359
|
+
if (!this.config.thinkingLevel && typeof config.effort === 'string' && config.effort) {
|
|
360
|
+
this.config.thinkingLevel = config.effort
|
|
361
|
+
}
|
|
362
|
+
// factory 注入的 agent 身份(无需等待 init())
|
|
363
|
+
this.selfAgentId = this.config.agentId ?? ''
|
|
364
|
+
this.agentName = this.config.name ?? 'agent'
|
|
365
|
+
this.agentRole = this.config.role ?? 'worker'
|
|
366
|
+
this.channelId = this.config.channelId ?? ''
|
|
367
|
+
this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
|
|
368
|
+
this.roster = createRosterCache({
|
|
369
|
+
selfAgentId: this.selfAgentId,
|
|
370
|
+
listAgents: () => this.workspace!.listAgents(),
|
|
371
|
+
})
|
|
372
|
+
liveAgents().add(this) // 在跑实例表:插件工具热注入的目标集
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ===== 生命周期 =====
|
|
376
|
+
|
|
377
|
+
async init(input: { agent: AgentInfo, channelId: string }): Promise<void> {
|
|
378
|
+
this.agentInfo = input.agent
|
|
379
|
+
this.channelId = input.channelId
|
|
380
|
+
this.agentName = input.agent.name
|
|
381
|
+
this.agentRole = input.agent.role
|
|
382
|
+
this.selfAgentId = input.agent.id
|
|
383
|
+
this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
|
|
384
|
+
this.roster = createRosterCache({
|
|
385
|
+
selfAgentId: this.selfAgentId,
|
|
386
|
+
listAgents: () => this.workspace!.listAgents(),
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async dispose(): Promise<void> {
|
|
391
|
+
liveAgents().delete(this)
|
|
392
|
+
if (this.client) {
|
|
393
|
+
const pid = this.client.pid
|
|
394
|
+
await this.client.dispose()
|
|
395
|
+
this.client = null
|
|
396
|
+
if (pid) {
|
|
397
|
+
markHarnessProcessExit(pid, null)
|
|
398
|
+
markTerminalSessionExit(pid, null)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
this.hostToolsRegistered = false
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** 工具注册表变更 → 热重发 set_host_tools(在跑会话立即获得插件新工具,无需重spawn) */
|
|
405
|
+
refreshPluginTools(): void {
|
|
406
|
+
const client = this.client
|
|
407
|
+
if (!client || !client.alive) return
|
|
408
|
+
void client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole, this.channelId) }).catch(() => {})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** harness 进程资源信息(运行时资源监控;进程未 spawn/已回收 → null) */
|
|
412
|
+
getProcessInfo(): { pid: number, alive: boolean, command: string } | null {
|
|
413
|
+
const client = this.client
|
|
414
|
+
const pid = client?.pid
|
|
415
|
+
if (!pid || !client) return null
|
|
416
|
+
return { pid, alive: client.alive, command: `omp --mode ${this.rpcMode}` }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** 强制终止 harness 进程(进程树;终止后由调用方停止对应 AgentRuntime) */
|
|
420
|
+
killProcess(): void {
|
|
421
|
+
const pid = this.client?.pid
|
|
422
|
+
if (pid) {
|
|
423
|
+
markTerminalSessionExit(pid, null)
|
|
424
|
+
killHarnessProcess(pid)
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
this.client?.kill()
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* harness 进程存活校准(manager sweeper 周期性调用;休眠/强杀后 exit 事件
|
|
433
|
+
* 可能不达父进程):按 PID 探 OS 实际存在性,进程已死 → 客户端收敛为已退出,
|
|
434
|
+
* 在途回合经 __process_exit__ 归位,下一回合 ensureClient 重生子进程。
|
|
435
|
+
*/
|
|
436
|
+
reconcileProcess(): void {
|
|
437
|
+
this.client?.reconcile()
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** omp 输出模式(rpc-ui = 默认,启用 HITL 对话框) */
|
|
441
|
+
private get rpcMode(): 'rpc' | 'rpc-ui' {
|
|
442
|
+
return this.config.rpcMode === 'rpc' ? 'rpc' : 'rpc-ui'
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ===== 工具桥(共享分发)=====
|
|
446
|
+
|
|
447
|
+
/** 引擎无关工具面:REST/MCP 桥直调与 omp 内部 host_tool_call 共用同一实现 */
|
|
448
|
+
dispatchHostTool(toolName: string, args: Record<string, unknown>): Promise<{ text: string, isError?: boolean }> {
|
|
449
|
+
return dispatchHostTool(this.bridgeCtx, { toolName, arguments: args })
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private async handleHostTool(req: HostToolCallRequest): Promise<{ text: string, isError?: boolean }> {
|
|
453
|
+
return this.dispatchHostTool(req.toolName, req.arguments ?? {})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ===== prompt 组合 =====
|
|
457
|
+
|
|
458
|
+
/** 前置上下文(场景 × 身份 × 工业简报 × 名册;共享 prompt-builder) */
|
|
459
|
+
private async contextPrefix(): Promise<string> {
|
|
460
|
+
return buildContextPrefix({
|
|
461
|
+
scenarioPrompt: this.config.scenarioPrompt,
|
|
462
|
+
systemPromptPrefix: this.config.systemPromptPrefix,
|
|
463
|
+
agentId: this.selfAgentId,
|
|
464
|
+
roster: await this.roster.roster(),
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private systemManual(): string {
|
|
469
|
+
return systemManual()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 实时消息注入(送达模式协议):
|
|
474
|
+
* - 回合 streaming 中 → 立即 steer,返回 'steer'(同轮可见,唯一可标记消费的路径)
|
|
475
|
+
* - 回合活跃但尚未 streaming(prompt 排队窗口,上限 20s)→ 等 streaming 开始后 steer
|
|
476
|
+
* - 回合被 host 工具阻塞(如 poll_messages 等待)/空闲/发送失败 → 返回 'deferred':
|
|
477
|
+
* 消息保持 pending,poll_messages 的 Mailbox 到信回调即时取走(毫秒级),或本回合
|
|
478
|
+
* 结束后由消费循环按 FIFO 起回合处理。
|
|
479
|
+
*/
|
|
480
|
+
async steer(text: string): Promise<'steer' | 'deferred'> {
|
|
481
|
+
// 从确定性触发横幅提取回执上下文(AgentRuntime.injectSteer 生成,格式固定):
|
|
482
|
+
// "[实时消息 from <id>]: ..." + "[系统触发器] 本消息要求回复(reply_to=<messageId>)。"
|
|
483
|
+
const banner = parseSteerBanner(text)
|
|
484
|
+
if (banner) {
|
|
485
|
+
this.toolState.replyContext = banner
|
|
486
|
+
}
|
|
487
|
+
const client = this.client
|
|
488
|
+
if (!client) return 'deferred'
|
|
489
|
+
try {
|
|
490
|
+
if (this.streaming) {
|
|
491
|
+
await client.send({ type: 'steer', message: text })
|
|
492
|
+
return 'steer'
|
|
493
|
+
}
|
|
494
|
+
if (this.turnActive) {
|
|
495
|
+
// prompt 排队窗口:等 streaming 开始(上限 20s);工具阻塞的回合等不到 → deferred
|
|
496
|
+
const deadline = Date.now() + 20_000
|
|
497
|
+
while (Date.now() < deadline && this.turnActive && !this.streaming) {
|
|
498
|
+
const { promise, resolve } = Promise.withResolvers()
|
|
499
|
+
setTimeout(resolve, 150)
|
|
500
|
+
await promise
|
|
501
|
+
}
|
|
502
|
+
if (this.streaming && this.turnActive) {
|
|
503
|
+
await client.send({ type: 'steer', message: text })
|
|
504
|
+
return 'steer'
|
|
505
|
+
}
|
|
506
|
+
return 'deferred'
|
|
507
|
+
}
|
|
508
|
+
// 空闲/回合已结束:deferred(消息 pending,消费循环 dequeue 即起回合)
|
|
509
|
+
return 'deferred'
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
log.error(`[OmpRpcAgent:${this.selfAgentId}] steer 注入失败(消息保持 pending):`, err instanceof Error ? err.message : err)
|
|
513
|
+
return 'deferred'
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async* run(request: AgentRunRequest, ctx: AgentRunContext): AsyncIterable<AgentEvent> {
|
|
518
|
+
const kind = request.message.metadata?.['x-aw-task-kind']
|
|
519
|
+
|
|
520
|
+
// worker: assign 消息 → 执行任务
|
|
521
|
+
if (kind === 'assign' && ctx.role === 'worker') {
|
|
522
|
+
yield* this.workerRun(request, ctx)
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// worker/lead: 同事点对点消息(含实时通信触发器)→ 按触发器语义处理并回复;
|
|
527
|
+
// 人类经 REST 注入的消息(from 空 + x-aw-from-label)同样进入应答流
|
|
528
|
+
if (!kind && (request.fromAgentId || request.message.metadata?.['x-aw-from-label'])) {
|
|
529
|
+
yield* this.peerMessageRun(request, ctx)
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 其余消息(lead 的 assign/child-completed 等):no-op(调度由 supervise() 处理)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ===== supervise() =====
|
|
537
|
+
|
|
538
|
+
/** supervise 单飞守卫:同一 client 不并发 LLM 回合(残留回合与下一 prompt 混流的根因) */
|
|
539
|
+
private supervising = false
|
|
540
|
+
|
|
541
|
+
async supervise(snapshot: SupervisionSnapshot, ctx: AgentRunContext, opts?: { signal?: AbortSignal }): Promise<SupervisionDecision[]> {
|
|
542
|
+
await this.ensureClient(ctx)
|
|
543
|
+
if (!this.client) return []
|
|
544
|
+
if (this.supervising) return [] // 上一轮 supervise 未收口:跳过本拍(节流即正确)
|
|
545
|
+
|
|
546
|
+
// 上下文门控(≥70% 先压缩再调度;回合间隙发起,失败放行)
|
|
547
|
+
await this.contextGate('supervise')
|
|
548
|
+
|
|
549
|
+
const prompt = supervisePrompt({
|
|
550
|
+
snapshot,
|
|
551
|
+
agentName: this.agentName,
|
|
552
|
+
channelId: this.channelId,
|
|
553
|
+
ctxPrefix: await this.contextPrefix(),
|
|
554
|
+
manual: this.systemManual(),
|
|
555
|
+
memory: ctx.memory,
|
|
556
|
+
})
|
|
557
|
+
// 150s 上界:supervise 持 lead.execLock 期间信箱消费停顿(更久会拖垮 worker 回执处理);
|
|
558
|
+
// supervise 是一次真实 LLM 回合:omp 冷启动(插件/MCP 加载 30~90s)+ 慢 provider
|
|
559
|
+
// 单步可能 >60s,过紧会把正常回合掐成 "Interrupted by user"。默认 150s,
|
|
560
|
+
// 仅拦真僵死(更紧的预算由调用方 config.superviseTimeoutMs 显式传入);真 abort 已实现。
|
|
561
|
+
const timeoutMs = this.config.superviseTimeoutMs ?? 150_000
|
|
562
|
+
|
|
563
|
+
return new Promise<SupervisionDecision[]>((resolve) => {
|
|
564
|
+
let assistantText = ''
|
|
565
|
+
let resolved = false
|
|
566
|
+
|
|
567
|
+
const finish = (decisions: SupervisionDecision[]) => {
|
|
568
|
+
if (resolved) return
|
|
569
|
+
resolved = true
|
|
570
|
+
this.supervising = false
|
|
571
|
+
unsub()
|
|
572
|
+
clearTimeout(timer)
|
|
573
|
+
signalUnsub?.()
|
|
574
|
+
resolve(decisions)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const abortTurn = (): void => {
|
|
578
|
+
// 超时/外部取消:真正中止 omp 当前回合 —— 只 resolve 不 abort 会让残留回合
|
|
579
|
+
// 与下一个 prompt 在同一 client 混流(决策错位 + token 空烧)
|
|
580
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] supervise 超时(${timeoutMs}ms)→ abort 当前调度回合`)
|
|
581
|
+
void this.client?.send({ type: 'abort' }).catch(() => {})
|
|
582
|
+
finish([])
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const unsub = this.client!.onEvent((event) => {
|
|
586
|
+
if (event.type === 'message_update' && event.assistantMessageEvent?.type === 'text_delta') {
|
|
587
|
+
assistantText += event.assistantMessageEvent.delta ?? ''
|
|
588
|
+
}
|
|
589
|
+
if (event.type === 'agent_end' && event.isTerminal !== false) {
|
|
590
|
+
// 尝试从文本解析 JSON 决策(备用:如果 agent 没用 host tools 而是输出 JSON)
|
|
591
|
+
const parsed = extractJsonArray(assistantText)
|
|
592
|
+
if (parsed && parsed.length > 0) {
|
|
593
|
+
finish(parsed as SupervisionDecision[])
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// agent 可能已通过 host tools 直接执行了调度,返回空(已执行)
|
|
597
|
+
finish([])
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (event.type === '__process_exit__' || event.type === '__error__') {
|
|
601
|
+
finish([])
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
const timer = setTimeout(abortTurn, timeoutMs)
|
|
606
|
+
|
|
607
|
+
// 外部取消(调度器 cancel 路径)传导:abort 当前 LLM 回合并立即收口
|
|
608
|
+
let signalUnsub: (() => void) | undefined
|
|
609
|
+
if (opts?.signal) {
|
|
610
|
+
if (opts.signal.aborted) {
|
|
611
|
+
unsub()
|
|
612
|
+
resolve([])
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
const onAbort = (): void => abortTurn()
|
|
616
|
+
opts.signal.addEventListener('abort', onAbort, { once: true })
|
|
617
|
+
signalUnsub = () => opts.signal?.removeEventListener('abort', onAbort)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
this.supervising = true
|
|
621
|
+
this.client!.send({ type: 'prompt', message: prompt }).catch(() => finish([]))
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ===== 内部:worker 执行 =====
|
|
626
|
+
|
|
627
|
+
private async* workerRun(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
|
|
628
|
+
const taskId = request.taskId
|
|
629
|
+
?? (request.message.metadata?.['x-aw-task-id'] as string | undefined)
|
|
630
|
+
if (!taskId) return
|
|
631
|
+
|
|
632
|
+
this.toolState.currentTaskId = taskId
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
await this.ensureClient(ctx)
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
yield {
|
|
639
|
+
kind: 'error',
|
|
640
|
+
error: {
|
|
641
|
+
code: 'OMP_SPAWN_FAILED',
|
|
642
|
+
message: `omp 子进程启动失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
643
|
+
},
|
|
644
|
+
}
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (!this.client) {
|
|
649
|
+
yield { kind: 'error', error: { code: 'OMP_NOT_READY', message: 'omp 客户端未就绪' } }
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 构建 prompt
|
|
654
|
+
const taskText = request.message.parts.map((p) => {
|
|
655
|
+
if ('text' in p) return p.text
|
|
656
|
+
if ('data' in p) return JSON.stringify(p.data)
|
|
657
|
+
if ('url' in p) return p.url
|
|
658
|
+
if ('raw' in p) return p.raw
|
|
659
|
+
return ''
|
|
660
|
+
}).join('\n')
|
|
661
|
+
const prompt = workerPrompt({
|
|
662
|
+
agentName: this.agentName,
|
|
663
|
+
channelId: this.channelId,
|
|
664
|
+
taskId,
|
|
665
|
+
taskText,
|
|
666
|
+
memory: request.memory,
|
|
667
|
+
ctxPrefix: await this.contextPrefix(),
|
|
668
|
+
manual: this.systemManual(),
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
// 流式执行 + 事件映射
|
|
672
|
+
yield* this.promptAndStream(prompt, taskId, ctx.signal)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* 点对点消息处理(实时通信驱动;lead 与 worker 通用)。
|
|
677
|
+
* 触发器语义:metadata['x-aw-require-reply']='true' → 必须经 send_message_to_agent
|
|
678
|
+
* 回给发送者:执行结果 + 对方所需内容,in_reply_to 关联原消息,并声明是否需再响应。
|
|
679
|
+
*/
|
|
680
|
+
private async* peerMessageRun(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
|
|
681
|
+
try {
|
|
682
|
+
await this.ensureClient(ctx)
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
// spawn 失败必须显式报错:静默 return 会让 sawRunError=false → 消息被
|
|
686
|
+
// 直接 markConsumed,实时消息一次性丢失且无任何重试;产出 error 事件
|
|
687
|
+
// 走消息 requeue(≤2 次),与 workerRun 同口径
|
|
688
|
+
yield {
|
|
689
|
+
kind: 'error',
|
|
690
|
+
error: {
|
|
691
|
+
code: 'OMP_SPAWN_FAILED',
|
|
692
|
+
message: `omp 子进程启动失败(peer 消息未消费,将重试): ${err instanceof Error ? err.message : String(err)}`,
|
|
693
|
+
},
|
|
694
|
+
}
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
if (!this.client) {
|
|
698
|
+
yield { kind: 'error', error: { code: 'OMP_NOT_READY', message: 'omp 客户端未就绪(peer 消息未消费,将重试)' } }
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const msg = request.message
|
|
703
|
+
// 显示名:agent 同事用 id(名册可解析);人类发送者用 fromLabel;兜底 unknown
|
|
704
|
+
const fromId = request.fromAgentId
|
|
705
|
+
?? (typeof msg.metadata?.['x-aw-from-label'] === 'string' ? msg.metadata['x-aw-from-label'] : undefined)
|
|
706
|
+
?? 'unknown'
|
|
707
|
+
const msgText = msg.parts.map((p) => {
|
|
708
|
+
if ('text' in p) return p.text
|
|
709
|
+
if ('data' in p) return JSON.stringify(p.data)
|
|
710
|
+
if ('url' in p) return p.url
|
|
711
|
+
if ('raw' in p) return p.raw
|
|
712
|
+
return ''
|
|
713
|
+
}).join('\n')
|
|
714
|
+
const requireReply = msg.metadata?.['x-aw-require-reply'] === 'true'
|
|
715
|
+
const isReply = typeof msg.metadata?.['x-aw-in-reply-to'] === 'string'
|
|
716
|
+
// 跨 Channel 来信:发送方是其他 channel 的 lead,回执必须走 send_cross_channel_message
|
|
717
|
+
const crossChannel = msg.metadata?.['x-aw-cross-channel'] === 'true'
|
|
718
|
+
const fromChannel = typeof msg.metadata?.['x-aw-from-channel'] === 'string'
|
|
719
|
+
? String(msg.metadata['x-aw-from-channel'])
|
|
720
|
+
: ''
|
|
721
|
+
// 回执自动关联仅对真实 agent 发送者生效(人类无 agentId 可回投;
|
|
722
|
+
// 对人类的回复经事件流/时间线可见)
|
|
723
|
+
this.toolState.replyContext = requireReply && request.fromAgentId && !crossChannel
|
|
724
|
+
? { fromId: request.fromAgentId, messageId: msg.messageId }
|
|
725
|
+
: null
|
|
726
|
+
|
|
727
|
+
const prompt = peerPrompt({
|
|
728
|
+
agentName: this.agentName,
|
|
729
|
+
role: this.agentRole,
|
|
730
|
+
channelId: this.channelId,
|
|
731
|
+
ctxPrefix: await this.contextPrefix(),
|
|
732
|
+
manual: this.systemManual(),
|
|
733
|
+
memory: request.memory,
|
|
734
|
+
fromId,
|
|
735
|
+
messageId: msg.messageId,
|
|
736
|
+
requireReply,
|
|
737
|
+
isReply,
|
|
738
|
+
crossChannel,
|
|
739
|
+
fromChannel,
|
|
740
|
+
msgText,
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
yield* this.promptAndStream(prompt, undefined, ctx.signal)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ===== 内部:prompt 发送 + 事件流桥接 =====
|
|
747
|
+
|
|
748
|
+
private async* promptAndStream(
|
|
749
|
+
prompt: string,
|
|
750
|
+
taskId: string | undefined,
|
|
751
|
+
signal: AbortSignal,
|
|
752
|
+
): AsyncGenerator<AgentEvent, void, unknown> {
|
|
753
|
+
const client = this.client
|
|
754
|
+
if (!client) {
|
|
755
|
+
throw new Error('omp 客户端未就绪')
|
|
756
|
+
}
|
|
757
|
+
// 上下文门控(≥70% 时在回合间隙先压缩再发 prompt;等待有界,失败放行)
|
|
758
|
+
await this.contextGate('pre-prompt')
|
|
759
|
+
const queue: AgentEvent[] = []
|
|
760
|
+
let resolveWait: (() => void) | null = null
|
|
761
|
+
let isDone = false
|
|
762
|
+
// 回合生命周期:prompt 已发出 → turnActive;首条 message_update → streaming;message_end/turn_end → 结束
|
|
763
|
+
this.turnActive = true
|
|
764
|
+
this.streaming = false
|
|
765
|
+
this.turnText = ''
|
|
766
|
+
// 正文流式差额账本:contentIndex → 已流出长度。部分 provider(reasoning 模型/
|
|
767
|
+
// openai-completions)的 text 块不走 text_delta(只有换行),全文在 text_end.content
|
|
768
|
+
// 落定 —— 差额兜底把未流出部分补发为 delta,保证前端流式不缺正文。
|
|
769
|
+
const textSent = new Map<number, number>()
|
|
770
|
+
const unsubState = client.onEvent((event) => {
|
|
771
|
+
if (event.type === 'message_update') {
|
|
772
|
+
const ev = event.assistantMessageEvent as
|
|
773
|
+
| { type?: string, delta?: string, content?: string, contentIndex?: number }
|
|
774
|
+
| undefined
|
|
775
|
+
if (ev?.type === 'text_delta') {
|
|
776
|
+
this.streaming = true
|
|
777
|
+
const text = ev.delta ?? ''
|
|
778
|
+
const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
|
|
779
|
+
textSent.set(ci, (textSent.get(ci) ?? 0) + text.length)
|
|
780
|
+
this.turnText += text
|
|
781
|
+
// LLM 流式增量透出(AEP agent.delta 事件源):仅 worker/peer 转本走生成器
|
|
782
|
+
if (text) enqueueDelta(text)
|
|
783
|
+
}
|
|
784
|
+
else if (ev?.type === 'text_end' && typeof ev.content === 'string') {
|
|
785
|
+
const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
|
|
786
|
+
const sent = textSent.get(ci) ?? 0
|
|
787
|
+
if (ev.content.length > sent) {
|
|
788
|
+
const chunk = ev.content.slice(sent)
|
|
789
|
+
textSent.set(ci, ev.content.length)
|
|
790
|
+
this.streaming = true
|
|
791
|
+
this.turnText += chunk
|
|
792
|
+
enqueueDelta(chunk)
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (event.type === 'message_end' || event.type === 'turn_end') {
|
|
797
|
+
this.streaming = false
|
|
798
|
+
flushDelta()
|
|
799
|
+
}
|
|
800
|
+
if (event.type === 'agent_end' && event.isTerminal !== false) {
|
|
801
|
+
this.turnActive = false
|
|
802
|
+
this.streaming = false
|
|
803
|
+
flushDelta()
|
|
804
|
+
}
|
|
805
|
+
if (event.type === '__process_exit__' || event.type === '__error__') {
|
|
806
|
+
this.turnActive = false
|
|
807
|
+
this.streaming = false
|
|
808
|
+
}
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
const enqueue = (event: AgentEvent): void => {
|
|
812
|
+
queue.push(event)
|
|
813
|
+
if (event.kind === 'done' || event.kind === 'error') isDone = true
|
|
814
|
+
lastActivity = Date.now()
|
|
815
|
+
resolveWait?.()
|
|
816
|
+
resolveWait = null
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// 停滞看门狗:整轮无任何 omp 事件(挂死的 LLM 调用/子进程僵死)时中止回合,
|
|
820
|
+
// 否则消息永久卡 consuming、队友消息无限堆积。host 工具内阻塞
|
|
821
|
+
// (poll_messages 最长 180s)与慢 provider(实测单步可 >5min)都有
|
|
822
|
+
// tool/状态事件刷新计时;默认 600s 覆盖慢回合,只拦真僵死。
|
|
823
|
+
const idleTimeoutMs = this.config.promptTimeoutMs ?? 600_000
|
|
824
|
+
let lastActivity = Date.now()
|
|
825
|
+
let stallTimer: ReturnType<typeof setTimeout> | null = null
|
|
826
|
+
|
|
827
|
+
// LLM 流式增量缓冲:50ms 批量合并为一帧 delta(防高频 text_delta 洪泛 WS)
|
|
828
|
+
let deltaBuf = ''
|
|
829
|
+
let deltaTimer: ReturnType<typeof setTimeout> | null = null
|
|
830
|
+
const flushDelta = (): void => {
|
|
831
|
+
if (deltaTimer) {
|
|
832
|
+
clearTimeout(deltaTimer)
|
|
833
|
+
deltaTimer = null
|
|
834
|
+
}
|
|
835
|
+
if (!deltaBuf) return
|
|
836
|
+
const text = deltaBuf
|
|
837
|
+
deltaBuf = ''
|
|
838
|
+
enqueue({ kind: 'delta', delta: { text } })
|
|
839
|
+
}
|
|
840
|
+
const enqueueDelta = (text: string): void => {
|
|
841
|
+
deltaBuf += text
|
|
842
|
+
deltaTimer ??= setTimeout(() => {
|
|
843
|
+
deltaTimer = null
|
|
844
|
+
flushDelta()
|
|
845
|
+
}, 50)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// 订阅 omp 事件流
|
|
849
|
+
const unsub = client.onEvent((event) => {
|
|
850
|
+
for (const mapped of this.mapOmpEvent(event, taskId)) {
|
|
851
|
+
enqueue(mapped)
|
|
852
|
+
}
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
// abort 传导
|
|
856
|
+
const onAbort = (): void => {
|
|
857
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] run 被 abort(signal)→ 中止 omp 回合,taskId=${taskId ?? '-'}`)
|
|
858
|
+
client.send({ type: 'abort' }).catch(() => {})
|
|
859
|
+
if (!isDone) {
|
|
860
|
+
enqueue({ kind: 'done', final: taskId ? { taskId } : undefined })
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (signal.aborted) {
|
|
864
|
+
onAbort()
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// 发送 prompt
|
|
871
|
+
try {
|
|
872
|
+
await client.send({ type: 'prompt', message: prompt })
|
|
873
|
+
}
|
|
874
|
+
catch (err) {
|
|
875
|
+
unsub()
|
|
876
|
+
signal.removeEventListener('abort', onAbort)
|
|
877
|
+
// 僵死进程回收:prompt send 失败 = stdio 断裂或 60s 命令确认超时 —— 进程
|
|
878
|
+
// "活着但不干活"(alive-but-wedged)。若仅报错不清理,消息重试(≤2 次)会
|
|
879
|
+
// 复用同一僵死 stdio 每次空转 60s;必须杀掉并置空,下回合 ensureClient
|
|
880
|
+
// 全新重生(host tools/模型/终端 tap 随重建)。
|
|
881
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] prompt 失败 → 回收可疑僵死进程 pid=${client.pid}`)
|
|
882
|
+
this.killProcess()
|
|
883
|
+
this.client = null
|
|
884
|
+
this.hostToolsRegistered = false
|
|
885
|
+
yield {
|
|
886
|
+
kind: 'error',
|
|
887
|
+
error: {
|
|
888
|
+
code: 'PROMPT_FAILED',
|
|
889
|
+
message: `omp prompt 失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
890
|
+
},
|
|
891
|
+
}
|
|
892
|
+
return
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// 产出事件直到 done/error
|
|
896
|
+
try {
|
|
897
|
+
while (!isDone || queue.length > 0) {
|
|
898
|
+
if (queue.length === 0 && !isDone) {
|
|
899
|
+
const remaining = idleTimeoutMs - (Date.now() - lastActivity)
|
|
900
|
+
if (remaining <= 0) {
|
|
901
|
+
client.send({ type: 'abort' }).catch(() => {})
|
|
902
|
+
// 子进程大概率已僵死:击杀并清引用,下回合 ensureClient 全新重生
|
|
903
|
+
this.killProcess()
|
|
904
|
+
this.client = null
|
|
905
|
+
this.hostToolsRegistered = false
|
|
906
|
+
enqueue({
|
|
907
|
+
kind: 'error',
|
|
908
|
+
error: {
|
|
909
|
+
code: 'TURN_STALLED',
|
|
910
|
+
message: `回合停滞 ${Math.round(idleTimeoutMs / 1000)}s 无 omp 事件,已中止重置(消息按已处理落账,后续消息继续)`,
|
|
911
|
+
},
|
|
912
|
+
})
|
|
913
|
+
continue
|
|
914
|
+
}
|
|
915
|
+
await new Promise<void>((r) => {
|
|
916
|
+
resolveWait = r
|
|
917
|
+
stallTimer = setTimeout(() => {
|
|
918
|
+
resolveWait = null
|
|
919
|
+
r()
|
|
920
|
+
}, remaining + 100)
|
|
921
|
+
})
|
|
922
|
+
if (stallTimer) {
|
|
923
|
+
clearTimeout(stallTimer)
|
|
924
|
+
stallTimer = null
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
while (queue.length > 0) {
|
|
928
|
+
yield queue.shift()!
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
finally {
|
|
933
|
+
if (stallTimer) clearTimeout(stallTimer)
|
|
934
|
+
// 事件监听必须全部解除:本 client 跨回合长期复用,残留的 mapOmpEvent
|
|
935
|
+
// 闭包会在后续每一帧继续执行并把事件 push 进孤儿队列(内存/CPU 随
|
|
936
|
+
// 回合数线性劣化,直接违背进程常驻的稳定性目标)
|
|
937
|
+
unsubState()
|
|
938
|
+
unsub()
|
|
939
|
+
signal.removeEventListener('abort', onAbort)
|
|
940
|
+
this.turnActive = false
|
|
941
|
+
this.streaming = false
|
|
942
|
+
this.toolState.currentTaskId = null
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ===== 内部:omp 事件 → AgentEvent 映射 =====
|
|
947
|
+
|
|
948
|
+
private mapOmpEvent(event: AgentSessionEvent, taskId: string | undefined): AgentEvent[] {
|
|
949
|
+
switch (event.type) {
|
|
950
|
+
case 'agent_start':
|
|
951
|
+
return [{
|
|
952
|
+
kind: 'status',
|
|
953
|
+
status: { state: 'WORKING', timestamp: new Date().toISOString() },
|
|
954
|
+
}]
|
|
955
|
+
case 'message_end': {
|
|
956
|
+
// 消息完成:如果有 message 内容,产出为 status 事件(任务历史追踪)
|
|
957
|
+
const msg = event.message
|
|
958
|
+
if (msg && Array.isArray(msg.content)) {
|
|
959
|
+
const text = (msg.content as Array<{ type?: string, text?: string }>)
|
|
960
|
+
.filter(c => c.type === 'text')
|
|
961
|
+
.map(c => c.text ?? '')
|
|
962
|
+
.join('')
|
|
963
|
+
if (text) {
|
|
964
|
+
const a2aMsg: A2AMessage = {
|
|
965
|
+
messageId: randomUUID(),
|
|
966
|
+
contextId: this.channelId,
|
|
967
|
+
role: 'ROLE_AGENT',
|
|
968
|
+
parts: [{ text }],
|
|
969
|
+
}
|
|
970
|
+
return [{
|
|
971
|
+
kind: 'status',
|
|
972
|
+
status: { state: 'WORKING', message: a2aMsg, timestamp: new Date().toISOString() },
|
|
973
|
+
}]
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return []
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
case 'tool_execution_start': {
|
|
980
|
+
const toolName = event.toolName ?? 'tool'
|
|
981
|
+
return [{
|
|
982
|
+
kind: 'status',
|
|
983
|
+
status: {
|
|
984
|
+
state: 'WORKING',
|
|
985
|
+
message: {
|
|
986
|
+
messageId: randomUUID(),
|
|
987
|
+
contextId: this.channelId,
|
|
988
|
+
role: 'ROLE_AGENT',
|
|
989
|
+
parts: [{ text: `🔧 ${toolName}${toolArgsPreview(event.args)}` }],
|
|
990
|
+
},
|
|
991
|
+
timestamp: new Date().toISOString(),
|
|
992
|
+
},
|
|
993
|
+
}]
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
case 'agent_end': {
|
|
997
|
+
if (event.isTerminal === false) return []
|
|
998
|
+
const events: AgentEvent[] = []
|
|
999
|
+
// 提取最终 assistant 文本作为 artifact
|
|
1000
|
+
const messages = event.messages ?? []
|
|
1001
|
+
const assistantText = messages
|
|
1002
|
+
.filter(m => m.role === 'assistant')
|
|
1003
|
+
.flatMap(m => m.content.filter(c => c.type === 'text').map(c => c.text ?? ''))
|
|
1004
|
+
.join('')
|
|
1005
|
+
// 回合内的 provider/API 失败(限额 429、鉴权、上游 5xx 等):omp 把错误挂在最后一条
|
|
1006
|
+
// assistant 消息上(stopReason='error' + errorStatus/errorMessage)而非 __error__ 帧。
|
|
1007
|
+
// 不映射出来,上层只能看到「回合结束无产出」,真实原因彻底丢失(且照常重试到耗尽)。
|
|
1008
|
+
const failure = [...messages].reverse().find((m) => {
|
|
1009
|
+
const mm = m as { role?: string, stopReason?: string, errorMessage?: string }
|
|
1010
|
+
return mm.role === 'assistant' && (mm.stopReason === 'error' || typeof mm.errorMessage === 'string')
|
|
1011
|
+
}) as { errorStatus?: number, errorMessage?: string } | undefined
|
|
1012
|
+
if (failure) {
|
|
1013
|
+
const code = failure.errorStatus != null ? `OMP_LLM_${failure.errorStatus}` : 'OMP_LLM_ERROR'
|
|
1014
|
+
const detail = failure.errorMessage ?? 'harness 回合以错误结束(无错误详情)'
|
|
1015
|
+
log.error(`[OmpRpcAgent:${this.selfAgentId}] harness 回合失败 ${code}: ${detail}`)
|
|
1016
|
+
// error 事件即回合终点(队列侧据此收口),不再追加 done
|
|
1017
|
+
return [{ kind: 'error', error: { code, message: detail } }]
|
|
1018
|
+
}
|
|
1019
|
+
if (assistantText) {
|
|
1020
|
+
events.push({
|
|
1021
|
+
kind: 'artifact',
|
|
1022
|
+
artifact: {
|
|
1023
|
+
artifactId: randomUUID(),
|
|
1024
|
+
name: 'output',
|
|
1025
|
+
parts: [{ text: assistantText }],
|
|
1026
|
+
},
|
|
1027
|
+
lastChunk: true,
|
|
1028
|
+
totalChunks: 1,
|
|
1029
|
+
})
|
|
1030
|
+
}
|
|
1031
|
+
events.push({ kind: 'done', final: taskId ? { taskId } : undefined })
|
|
1032
|
+
return events
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
case '__process_exit__':
|
|
1036
|
+
return [{
|
|
1037
|
+
kind: 'error',
|
|
1038
|
+
error: { code: 'OMP_PROCESS_EXIT', message: 'omp 子进程意外退出' },
|
|
1039
|
+
}]
|
|
1040
|
+
|
|
1041
|
+
// ===== 上下文治理事件(回合中到达的原生阈值/overflow 压缩;不污染事件流)=====
|
|
1042
|
+
case 'compaction_start':
|
|
1043
|
+
this.compacting = true
|
|
1044
|
+
return []
|
|
1045
|
+
|
|
1046
|
+
case 'compaction_end': {
|
|
1047
|
+
this.compacting = false
|
|
1048
|
+
const result = (event as { result?: CompactionResult }).result
|
|
1049
|
+
if (result?.summary) {
|
|
1050
|
+
void this.harvestCompaction(
|
|
1051
|
+
result.summary,
|
|
1052
|
+
result.tokensBefore,
|
|
1053
|
+
result.tokensAfter ?? result.estimatedTokensAfter,
|
|
1054
|
+
(event as { reason?: string }).reason,
|
|
1055
|
+
)
|
|
1056
|
+
}
|
|
1057
|
+
return []
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// 回合完全落定(无自动重试/压缩重试/排队续跑):后续压缩检查由 AgentRuntime 驱动
|
|
1061
|
+
case 'agent_settled':
|
|
1062
|
+
return []
|
|
1063
|
+
|
|
1064
|
+
case '__error__':
|
|
1065
|
+
return [{
|
|
1066
|
+
kind: 'error',
|
|
1067
|
+
error: { code: 'OMP_ERROR', message: (event as { error?: string }).error ?? 'omp 未知错误' },
|
|
1068
|
+
}]
|
|
1069
|
+
|
|
1070
|
+
default:
|
|
1071
|
+
return []
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ===== 内部:omp 客户端管理 =====
|
|
1076
|
+
|
|
1077
|
+
private async ensureClient(ctx: AgentRunContext): Promise<void> {
|
|
1078
|
+
if (!this.workspace) {
|
|
1079
|
+
this.workspace = ctx.workspace
|
|
1080
|
+
}
|
|
1081
|
+
if (!this.agentInfo) {
|
|
1082
|
+
// init() 可能未调用;从 ctx 推断
|
|
1083
|
+
this.channelId = ctx.channelId
|
|
1084
|
+
this.agentRole = ctx.role
|
|
1085
|
+
this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (!this.client || !this.client.alive) {
|
|
1089
|
+
// 旧客户端已退出(exit 事件/OS 存活校准 reconcile 触发)——必须丢弃并重生,
|
|
1090
|
+
// 否则后续回合会一直对着死 stdio 报 PROMPT_FAILED 烧重试配额
|
|
1091
|
+
this.client = null
|
|
1092
|
+
this.hostToolsRegistered = false
|
|
1093
|
+
const command = this.config.command ?? 'omp'
|
|
1094
|
+
const client = new OmpRpcClient({
|
|
1095
|
+
command,
|
|
1096
|
+
mode: this.rpcMode,
|
|
1097
|
+
args: [...(this.config.thinkingLevel ? ['--thinking', this.config.thinkingLevel] : [])],
|
|
1098
|
+
args: this.config.args,
|
|
1099
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
1100
|
+
// 进程退出 → 注册表标记(供运行时资源监控);pid=-1 表示无法取得,忽略
|
|
1101
|
+
onExit: (pid, code) => {
|
|
1102
|
+
if (pid > 0) {
|
|
1103
|
+
markHarnessProcessExit(pid, code)
|
|
1104
|
+
markTerminalSessionExit(pid, code)
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
})
|
|
1108
|
+
await client.start()
|
|
1109
|
+
|
|
1110
|
+
// 登记 + 绑定 agent 身份(harness 进程监控的事实源)
|
|
1111
|
+
const pid = client.pid
|
|
1112
|
+
if (pid) {
|
|
1113
|
+
registerHarnessProcess(pid, {
|
|
1114
|
+
harness: 'omp',
|
|
1115
|
+
command,
|
|
1116
|
+
args: ['--mode', this.rpcMode, ...(this.config.args ?? [])],
|
|
1117
|
+
})
|
|
1118
|
+
bindHarnessProcess(pid, {
|
|
1119
|
+
agentId: this.selfAgentId,
|
|
1120
|
+
channelId: this.channelId,
|
|
1121
|
+
name: this.agentName,
|
|
1122
|
+
role: this.agentRole,
|
|
1123
|
+
})
|
|
1124
|
+
// 终端镜像 tap:全部 RPC 帧 → /monitor 终端(实时 TUI 渲染 + HITL)
|
|
1125
|
+
attachTerminalTap(client, {
|
|
1126
|
+
pid,
|
|
1127
|
+
harness: 'omp',
|
|
1128
|
+
agentId: this.selfAgentId,
|
|
1129
|
+
channelId: this.channelId,
|
|
1130
|
+
name: this.agentName,
|
|
1131
|
+
role: this.agentRole,
|
|
1132
|
+
})
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// 设置模型(如果配置了 provider/model 任一;omp 以当前已设值为缺省补全,
|
|
1136
|
+
// 只配其一时也显式发送,避免 respawn 后静默回落默认模型)
|
|
1137
|
+
if (this.config.provider || this.config.model) {
|
|
1138
|
+
try {
|
|
1139
|
+
// 未配置一侧保持 undefined(JSON 序列化丢键)→ omp 以当前已设值补全
|
|
1140
|
+
await client.send({
|
|
1141
|
+
type: 'set_model',
|
|
1142
|
+
provider: this.config.provider as string,
|
|
1143
|
+
modelId: this.config.model as string,
|
|
1144
|
+
})
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
// 模型设置失败不致命(用 omp 默认模型)
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// 注册 host tools(按角色差异化:lead 全量,worker 剔除调度/团队管理专属工具)
|
|
1152
|
+
client.onHostToolCall(req => this.handleHostTool(req))
|
|
1153
|
+
await client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole, this.channelId) })
|
|
1154
|
+
|
|
1155
|
+
// 上下文治理探测(feature-detect 一次;失败退化被动 usage 跟踪)+ 原生压缩兜底保持开启
|
|
1156
|
+
void this.probeContext(client).catch(() => {})
|
|
1157
|
+
void client.send({ type: 'set_auto_compaction', enabled: true }).catch(() => {})
|
|
1158
|
+
|
|
1159
|
+
this.client = client
|
|
1160
|
+
this.hostToolsRegistered = true
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/** 上下文状态探测:get_state 取 contextWindow/sessionId(一次;失败仅损失 percent 精度) */
|
|
1165
|
+
private async probeContext(client: OmpRpcClient): Promise<void> {
|
|
1166
|
+
try {
|
|
1167
|
+
const resp = await client.send({ type: 'get_state' })
|
|
1168
|
+
const data = (resp.data ?? {}) as Record<string, unknown>
|
|
1169
|
+
const model = data.model as { contextWindow?: number } | undefined
|
|
1170
|
+
if (typeof model?.contextWindow === 'number' && model.contextWindow > 0) {
|
|
1171
|
+
this.contextWindow = model.contextWindow
|
|
1172
|
+
client.setContextWindow(model.contextWindow)
|
|
1173
|
+
}
|
|
1174
|
+
if (typeof data.sessionId === 'string' && data.sessionId) this.sessionId = data.sessionId
|
|
1175
|
+
}
|
|
1176
|
+
catch { /* get_state 不可用:仅被动 usage */ }
|
|
1177
|
+
}
|
|
1178
|
+
}
|