agim-cli 1.3.4 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -6
- package/dist/cli-ui/tui/app.js +1 -1
- package/dist/cli-ui/tui/app.js.map +1 -1
- package/dist/cli.js +25 -4
- package/dist/cli.js.map +1 -1
- package/dist/core/a2a.d.ts.map +1 -1
- package/dist/core/a2a.js +6 -0
- package/dist/core/a2a.js.map +1 -1
- package/dist/core/access-token.d.ts.map +1 -1
- package/dist/core/access-token.js +16 -4
- package/dist/core/access-token.js.map +1 -1
- package/dist/core/approval-bus.d.ts +5 -0
- package/dist/core/approval-bus.d.ts.map +1 -1
- package/dist/core/approval-bus.js +32 -0
- package/dist/core/approval-bus.js.map +1 -1
- package/dist/core/commands/job.d.ts.map +1 -1
- package/dist/core/commands/job.js +10 -3
- package/dist/core/commands/job.js.map +1 -1
- package/dist/core/goal-rpc.d.ts +7 -0
- package/dist/core/goal-rpc.d.ts.map +1 -1
- package/dist/core/goal-rpc.js +47 -3
- package/dist/core/goal-rpc.js.map +1 -1
- package/dist/core/llm/exec-dispatcher.d.ts +2 -0
- package/dist/core/llm/exec-dispatcher.d.ts.map +1 -1
- package/dist/core/llm/exec-dispatcher.js +60 -6
- package/dist/core/llm/exec-dispatcher.js.map +1 -1
- package/dist/core/llm/fs-dispatcher.d.ts +3 -0
- package/dist/core/llm/fs-dispatcher.d.ts.map +1 -1
- package/dist/core/llm/fs-dispatcher.js +31 -12
- package/dist/core/llm/fs-dispatcher.js.map +1 -1
- package/dist/core/llm/imhub-dispatcher.d.ts.map +1 -1
- package/dist/core/llm/imhub-dispatcher.js +9 -5
- package/dist/core/llm/imhub-dispatcher.js.map +1 -1
- package/dist/core/llm/model-catalog.js +1 -1
- package/dist/core/llm/model-catalog.js.map +1 -1
- package/dist/core/llm/policy-approval-gate.d.ts.map +1 -1
- package/dist/core/llm/policy-approval-gate.js +17 -4
- package/dist/core/llm/policy-approval-gate.js.map +1 -1
- package/dist/core/llm/registry.d.ts +2 -2
- package/dist/core/llm/registry.js +1 -1
- package/dist/core/llm/web-dispatcher.d.ts +20 -0
- package/dist/core/llm/web-dispatcher.d.ts.map +1 -1
- package/dist/core/llm/web-dispatcher.js +70 -5
- package/dist/core/llm/web-dispatcher.js.map +1 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +5 -19
- package/dist/core/registry.js.map +1 -1
- package/dist/core/router.js +1 -1
- package/dist/core/router.js.map +1 -1
- package/dist/core/schedule.d.ts.map +1 -1
- package/dist/core/schedule.js +6 -2
- package/dist/core/schedule.js.map +1 -1
- package/dist/core/skills/loader.js +3 -3
- package/dist/core/skills/loader.js.map +1 -1
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/plugins/agents/native/index.d.ts +0 -146
- package/dist/plugins/agents/native/index.d.ts.map +1 -1
- package/dist/plugins/agents/native/index.js +41 -1291
- package/dist/plugins/agents/native/index.js.map +1 -1
- package/dist/plugins/agents/pi-native/approval.d.ts.map +1 -1
- package/dist/plugins/agents/pi-native/approval.js +2 -3
- package/dist/plugins/agents/pi-native/approval.js.map +1 -1
- package/dist/plugins/agents/pi-native/factory.d.ts +3 -4
- package/dist/plugins/agents/pi-native/factory.d.ts.map +1 -1
- package/dist/plugins/agents/pi-native/factory.js +8 -14
- package/dist/plugins/agents/pi-native/factory.js.map +1 -1
- package/dist/plugins/agents/pi-native/index.d.ts +8 -8
- package/dist/plugins/agents/pi-native/index.d.ts.map +1 -1
- package/dist/plugins/agents/pi-native/index.js +78 -13
- package/dist/plugins/agents/pi-native/index.js.map +1 -1
- package/dist/plugins/agents/pi-native/provider-resolver.d.ts +4 -4
- package/dist/plugins/agents/pi-native/provider-resolver.d.ts.map +1 -1
- package/dist/plugins/agents/pi-native/provider-resolver.js +1 -1
- package/dist/plugins/agents/pi-native/provider-resolver.js.map +1 -1
- package/dist/plugins/agents/pi-native/tool-bridge.js +1 -1
- package/dist/plugins/agents/pi-native/tool-bridge.js.map +1 -1
- package/dist/plugins/agents/pi-native/tools.d.ts +3 -4
- package/dist/plugins/agents/pi-native/tools.d.ts.map +1 -1
- package/dist/plugins/agents/pi-native/tools.js +7 -9
- package/dist/plugins/agents/pi-native/tools.js.map +1 -1
- package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -1
- package/dist/plugins/messengers/discord/discord-adapter.js +14 -2
- package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -1
- package/dist/web/public/assets/{a2a-Y30Sn8pc.js → a2a-Bk51ozUt.js} +2 -2
- package/dist/web/public/assets/{a2a-Y30Sn8pc.js.map → a2a-Bk51ozUt.js.map} +1 -1
- package/dist/web/public/assets/{activity--oN7qxFJ.js → activity-DzKONPpA.js} +2 -2
- package/dist/web/public/assets/{activity--oN7qxFJ.js.map → activity-DzKONPpA.js.map} +1 -1
- package/dist/web/public/assets/{admins-DONiT3ld.js → admins-CmZ4eQnt.js} +2 -2
- package/dist/web/public/assets/{admins-DONiT3ld.js.map → admins-CmZ4eQnt.js.map} +1 -1
- package/dist/web/public/assets/{agents-DRbQOH0K.js → agents-lj76gn9U.js} +2 -2
- package/dist/web/public/assets/{agents-DRbQOH0K.js.map → agents-lj76gn9U.js.map} +1 -1
- package/dist/web/public/assets/{approvals-C5jd7Wf9.js → approvals-C5-3WBcb.js} +2 -2
- package/dist/web/public/assets/{approvals-C5jd7Wf9.js.map → approvals-C5-3WBcb.js.map} +1 -1
- package/dist/web/public/assets/{arrow-down-x-Lq92tn.js → arrow-down-DAjYgwW2.js} +2 -2
- package/dist/web/public/assets/{arrow-down-x-Lq92tn.js.map → arrow-down-DAjYgwW2.js.map} +1 -1
- package/dist/web/public/assets/{arrow-up-Cc28qbjN.js → arrow-up-Cba9TuAO.js} +2 -2
- package/dist/web/public/assets/{arrow-up-Cc28qbjN.js.map → arrow-up-Cba9TuAO.js.map} +1 -1
- package/dist/web/public/assets/{asks-CeVcu8SD.js → asks-qlttx_de.js} +2 -2
- package/dist/web/public/assets/{asks-CeVcu8SD.js.map → asks-qlttx_de.js.map} +1 -1
- package/dist/web/public/assets/{audit-Dc9_FDkW.js → audit-CSjpfK6P.js} +2 -2
- package/dist/web/public/assets/{audit-Dc9_FDkW.js.map → audit-CSjpfK6P.js.map} +1 -1
- package/dist/web/public/assets/{bell-CxPf_Xzs.js → bell-Dm9oEZR5.js} +2 -2
- package/dist/web/public/assets/{bell-CxPf_Xzs.js.map → bell-Dm9oEZR5.js.map} +1 -1
- package/dist/web/public/assets/{bgjobs-CYjmAJgs.js → bgjobs-IOX_MrKV.js} +2 -2
- package/dist/web/public/assets/{bgjobs-CYjmAJgs.js.map → bgjobs-IOX_MrKV.js.map} +1 -1
- package/dist/web/public/assets/{brain-DjCfwBAT.js → brain-D24bSBeM.js} +2 -2
- package/dist/web/public/assets/{brain-DjCfwBAT.js.map → brain-D24bSBeM.js.map} +1 -1
- package/dist/web/public/assets/{briefcase-Br1bd_VG.js → briefcase-BrIzC9EZ.js} +2 -2
- package/dist/web/public/assets/{briefcase-Br1bd_VG.js.map → briefcase-BrIzC9EZ.js.map} +1 -1
- package/dist/web/public/assets/{chat-uqxuXn2X.js → chat-DXki5zbL.js} +2 -2
- package/dist/web/public/assets/{chat-uqxuXn2X.js.map → chat-DXki5zbL.js.map} +1 -1
- package/dist/web/public/assets/{chevron-left-CN-4SIfi.js → chevron-left-C8Ha_lMZ.js} +2 -2
- package/dist/web/public/assets/{chevron-left-CN-4SIfi.js.map → chevron-left-C8Ha_lMZ.js.map} +1 -1
- package/dist/web/public/assets/{chevron-right-BhSTLH2K.js → chevron-right-LF_j8BGd.js} +2 -2
- package/dist/web/public/assets/{chevron-right-BhSTLH2K.js.map → chevron-right-LF_j8BGd.js.map} +1 -1
- package/dist/web/public/assets/{circle-check-B4UdHmdJ.js → circle-check-CpSR3rJi.js} +2 -2
- package/dist/web/public/assets/{circle-check-B4UdHmdJ.js.map → circle-check-CpSR3rJi.js.map} +1 -1
- package/dist/web/public/assets/{circle-check-big-CvXnDqiF.js → circle-check-big-Dbz0XziN.js} +2 -2
- package/dist/web/public/assets/{circle-check-big-CvXnDqiF.js.map → circle-check-big-Dbz0XziN.js.map} +1 -1
- package/dist/web/public/assets/{circle-x-B8y8mLHr.js → circle-x-XWMjbfp5.js} +2 -2
- package/dist/web/public/assets/{circle-x-B8y8mLHr.js.map → circle-x-XWMjbfp5.js.map} +1 -1
- package/dist/web/public/assets/{clock-CK2KO_Rc.js → clock-BjVFYz37.js} +2 -2
- package/dist/web/public/assets/{clock-CK2KO_Rc.js.map → clock-BjVFYz37.js.map} +1 -1
- package/dist/web/public/assets/{confirm-dialog-GZyIzSwj.js → confirm-dialog-C8nuBNh3.js} +2 -2
- package/dist/web/public/assets/{confirm-dialog-GZyIzSwj.js.map → confirm-dialog-C8nuBNh3.js.map} +1 -1
- package/dist/web/public/assets/{copy-CDCgL5Yq.js → copy-BUxvUnbP.js} +2 -2
- package/dist/web/public/assets/{copy-CDCgL5Yq.js.map → copy-BUxvUnbP.js.map} +1 -1
- package/dist/web/public/assets/{data-table-1cGunUIX.js → data-table-BHctKsHM.js} +2 -2
- package/dist/web/public/assets/{data-table-1cGunUIX.js.map → data-table-BHctKsHM.js.map} +1 -1
- package/dist/web/public/assets/{download-CuLP0G-q.js → download-C0hikBL7.js} +2 -2
- package/dist/web/public/assets/{download-CuLP0G-q.js.map → download-C0hikBL7.js.map} +1 -1
- package/dist/web/public/assets/{email-Q-SY9qC9.js → email-D940p9kF.js} +2 -2
- package/dist/web/public/assets/{email-Q-SY9qC9.js.map → email-D940p9kF.js.map} +1 -1
- package/dist/web/public/assets/{empty-state-BRsb30K6.js → empty-state-BzzUgjlL.js} +2 -2
- package/dist/web/public/assets/{empty-state-BRsb30K6.js.map → empty-state-BzzUgjlL.js.map} +1 -1
- package/dist/web/public/assets/{external-link-D-E90Kfw.js → external-link-Cjuz11n7.js} +2 -2
- package/dist/web/public/assets/{external-link-D-E90Kfw.js.map → external-link-Cjuz11n7.js.map} +1 -1
- package/dist/web/public/assets/{eye-DSu_4mW2.js → eye-DVQjbb2Z.js} +2 -2
- package/dist/web/public/assets/{eye-DSu_4mW2.js.map → eye-DVQjbb2Z.js.map} +1 -1
- package/dist/web/public/assets/{facts-pmGTfGmv.js → facts-C4hVaO6t.js} +2 -2
- package/dist/web/public/assets/{facts-pmGTfGmv.js.map → facts-C4hVaO6t.js.map} +1 -1
- package/dist/web/public/assets/{goals-DpRm9-av.js → goals-89sYO75m.js} +2 -2
- package/dist/web/public/assets/{goals-DpRm9-av.js.map → goals-89sYO75m.js.map} +1 -1
- package/dist/web/public/assets/{health-mqoH7KDV.js → health-CKfZBddX.js} +2 -2
- package/dist/web/public/assets/{health-mqoH7KDV.js.map → health-CKfZBddX.js.map} +1 -1
- package/dist/web/public/assets/{heart-pulse-CrCHhXtC.js → heart-pulse-BfYs3KFD.js} +2 -2
- package/dist/web/public/assets/{heart-pulse-CrCHhXtC.js.map → heart-pulse-BfYs3KFD.js.map} +1 -1
- package/dist/web/public/assets/{heartbeat-EGjLHWU0.js → heartbeat-oKkyHWgV.js} +2 -2
- package/dist/web/public/assets/{heartbeat-EGjLHWU0.js.map → heartbeat-oKkyHWgV.js.map} +1 -1
- package/dist/web/public/assets/{hot-B9x1_5yx.js → hot-Cas_nZv9.js} +2 -2
- package/dist/web/public/assets/{hot-B9x1_5yx.js.map → hot-Cas_nZv9.js.map} +1 -1
- package/dist/web/public/assets/{index-BIBWEKxu.js → index-B795P_3p.js} +34 -34
- package/dist/web/public/assets/index-B795P_3p.js.map +1 -0
- package/dist/web/public/assets/{index-Bc0H0I1S.css → index-CokfQGcz.css} +1 -1
- package/dist/web/public/assets/{injection-CDx5n-mk.js → injection-DksSjIsi.js} +2 -2
- package/dist/web/public/assets/{injection-CDx5n-mk.js.map → injection-DksSjIsi.js.map} +1 -1
- package/dist/web/public/assets/{installed-DV7X4wfv.js → installed-DWq0Eluy.js} +2 -2
- package/dist/web/public/assets/{installed-DV7X4wfv.js.map → installed-DWq0Eluy.js.map} +1 -1
- package/dist/web/public/assets/{jobs-zAigElCl.js → jobs-CqY-xtBr.js} +2 -2
- package/dist/web/public/assets/{jobs-zAigElCl.js.map → jobs-CqY-xtBr.js.map} +1 -1
- package/dist/web/public/assets/{layout-B0RjH6M2.js → layout-BiAVhWgU.js} +2 -2
- package/dist/web/public/assets/{layout-B0RjH6M2.js.map → layout-BiAVhWgU.js.map} +1 -1
- package/dist/web/public/assets/{layout-DOSpVfND.js → layout-BodTIhhE.js} +2 -2
- package/dist/web/public/assets/{layout-DOSpVfND.js.map → layout-BodTIhhE.js.map} +1 -1
- package/dist/web/public/assets/{layout-CzjqlPxW.js → layout-Cltp3ikG.js} +2 -2
- package/dist/web/public/assets/{layout-CzjqlPxW.js.map → layout-Cltp3ikG.js.map} +1 -1
- package/dist/web/public/assets/{layout-C6q-GbBZ.js → layout-CqgDOB5t.js} +2 -2
- package/dist/web/public/assets/{layout-C6q-GbBZ.js.map → layout-CqgDOB5t.js.map} +1 -1
- package/dist/web/public/assets/{layout-7wcifbsm.js → layout-DsoDxB0i.js} +2 -2
- package/dist/web/public/assets/{layout-7wcifbsm.js.map → layout-DsoDxB0i.js.map} +1 -1
- package/dist/web/public/assets/{llm-DwZX5fwY.js → llm-D3Kd9gqa.js} +2 -2
- package/dist/web/public/assets/{llm-DwZX5fwY.js.map → llm-D3Kd9gqa.js.map} +1 -1
- package/dist/web/public/assets/{loader-circle-BQkLEJZw.js → loader-circle-DsLQMvHE.js} +2 -2
- package/dist/web/public/assets/{loader-circle-BQkLEJZw.js.map → loader-circle-DsLQMvHE.js.map} +1 -1
- package/dist/web/public/assets/{map-pin-B-IMY07m.js → map-pin-e694Eet6.js} +2 -2
- package/dist/web/public/assets/{map-pin-B-IMY07m.js.map → map-pin-e694Eet6.js.map} +1 -1
- package/dist/web/public/assets/{mcp-K0P3-b6q.js → mcp-BoWJqog4.js} +2 -2
- package/dist/web/public/assets/{mcp-K0P3-b6q.js.map → mcp-BoWJqog4.js.map} +1 -1
- package/dist/web/public/assets/{memos-DcpDEVN8.js → memos-C81bimPJ.js} +2 -2
- package/dist/web/public/assets/{memos-DcpDEVN8.js.map → memos-C81bimPJ.js.map} +1 -1
- package/dist/web/public/assets/{messengers-DmrNvPMS.js → messengers-CGcjXUeJ.js} +2 -2
- package/dist/web/public/assets/{messengers-DmrNvPMS.js.map → messengers-CGcjXUeJ.js.map} +1 -1
- package/dist/web/public/assets/{mobile-a-oUiAoR.js → mobile-Duy_If5A.js} +2 -2
- package/dist/web/public/assets/{mobile-a-oUiAoR.js.map → mobile-Duy_If5A.js.map} +1 -1
- package/dist/web/public/assets/{network-Bbh3j5ew.js → network-DnBhCncp.js} +2 -2
- package/dist/web/public/assets/{network-Bbh3j5ew.js.map → network-DnBhCncp.js.map} +1 -1
- package/dist/web/public/assets/{outbox-BP_hfANb.js → outbox-DrReVtHr.js} +2 -2
- package/dist/web/public/assets/{outbox-BP_hfANb.js.map → outbox-DrReVtHr.js.map} +1 -1
- package/dist/web/public/assets/{pagination-CPLj3V0r.js → pagination-DgS8vaZM.js} +2 -2
- package/dist/web/public/assets/{pagination-CPLj3V0r.js.map → pagination-DgS8vaZM.js.map} +1 -1
- package/dist/web/public/assets/{persona-CT38dLd4.js → persona-NTg-RuPa.js} +2 -2
- package/dist/web/public/assets/{persona-CT38dLd4.js.map → persona-NTg-RuPa.js.map} +1 -1
- package/dist/web/public/assets/{plans-Msw85XkI.js → plans-CUsr4aMb.js} +2 -2
- package/dist/web/public/assets/{plans-Msw85XkI.js.map → plans-CUsr4aMb.js.map} +1 -1
- package/dist/web/public/assets/{play-tH2nb1Kt.js → play-G96k2QBR.js} +2 -2
- package/dist/web/public/assets/{play-tH2nb1Kt.js.map → play-G96k2QBR.js.map} +1 -1
- package/dist/web/public/assets/{plus-6MNGQidu.js → plus-6RZ3XScp.js} +2 -2
- package/dist/web/public/assets/{plus-6MNGQidu.js.map → plus-6RZ3XScp.js.map} +1 -1
- package/dist/web/public/assets/{policy-BJ7zORVG.js → policy-BFlQnPo5.js} +2 -2
- package/dist/web/public/assets/{policy-BJ7zORVG.js.map → policy-BFlQnPo5.js.map} +1 -1
- package/dist/web/public/assets/{qr-code-DfsxFxoa.js → qr-code-L7ClZ31U.js} +2 -2
- package/dist/web/public/assets/{qr-code-DfsxFxoa.js.map → qr-code-L7ClZ31U.js.map} +1 -1
- package/dist/web/public/assets/{refresh-ccw-CFPE6zD1.js → refresh-ccw-S3sFYfDb.js} +2 -2
- package/dist/web/public/assets/{refresh-ccw-CFPE6zD1.js.map → refresh-ccw-S3sFYfDb.js.map} +1 -1
- package/dist/web/public/assets/{reminders-CAUNAuDy.js → reminders-DVzHdBpK.js} +2 -2
- package/dist/web/public/assets/{reminders-CAUNAuDy.js.map → reminders-DVzHdBpK.js.map} +1 -1
- package/dist/web/public/assets/{save-CNUmUMEf.js → save-DetwgDjo.js} +2 -2
- package/dist/web/public/assets/{save-CNUmUMEf.js.map → save-DetwgDjo.js.map} +1 -1
- package/dist/web/public/assets/{schedules-C25NyoGh.js → schedules-CslP2Ntr.js} +2 -2
- package/dist/web/public/assets/{schedules-C25NyoGh.js.map → schedules-CslP2Ntr.js.map} +1 -1
- package/dist/web/public/assets/{search-D9TZs0bG.js → search-Bx2TNvqF.js} +2 -2
- package/dist/web/public/assets/{search-D9TZs0bG.js.map → search-Bx2TNvqF.js.map} +1 -1
- package/dist/web/public/assets/{search-CaRTqZw1.js → search-ByHc__pY.js} +2 -2
- package/dist/web/public/assets/{search-CaRTqZw1.js.map → search-ByHc__pY.js.map} +1 -1
- package/dist/web/public/assets/{security-BMGGzEwr.js → security-B4dw2-ZU.js} +2 -2
- package/dist/web/public/assets/{security-BMGGzEwr.js.map → security-B4dw2-ZU.js.map} +1 -1
- package/dist/web/public/assets/{service-BQBoddcT.js → service-LujRx8DT.js} +2 -2
- package/dist/web/public/assets/{service-BQBoddcT.js.map → service-LujRx8DT.js.map} +1 -1
- package/dist/web/public/assets/{shield-alert-eOBoc2yc.js → shield-alert-BM2sbwiD.js} +2 -2
- package/dist/web/public/assets/{shield-alert-eOBoc2yc.js.map → shield-alert-BM2sbwiD.js.map} +1 -1
- package/dist/web/public/assets/{status-badge-CF1Cya26.js → status-badge-Bz0a-nE1.js} +2 -2
- package/dist/web/public/assets/{status-badge-CF1Cya26.js.map → status-badge-Bz0a-nE1.js.map} +1 -1
- package/dist/web/public/assets/{subtasks-CEPB2TSV.js → subtasks-BEAppda9.js} +2 -2
- package/dist/web/public/assets/{subtasks-CEPB2TSV.js.map → subtasks-BEAppda9.js.map} +1 -1
- package/dist/web/public/assets/{table-Dww-k14l.js → table-Dl8Faxz-.js} +2 -2
- package/dist/web/public/assets/{table-Dww-k14l.js.map → table-Dl8Faxz-.js.map} +1 -1
- package/dist/web/public/assets/{topn-DwgKlIxc.js → topn-CXTn3Qdn.js} +2 -2
- package/dist/web/public/assets/{topn-DwgKlIxc.js.map → topn-CXTn3Qdn.js.map} +1 -1
- package/dist/web/public/assets/{trash-2-BF98SY8M.js → trash-2-DUSLukqd.js} +2 -2
- package/dist/web/public/assets/{trash-2-BF98SY8M.js.map → trash-2-DUSLukqd.js.map} +1 -1
- package/dist/web/public/assets/{use-agim-skills-kDpiSMBP.js → use-agim-skills-BD1AIi8d.js} +2 -2
- package/dist/web/public/assets/{use-agim-skills-kDpiSMBP.js.map → use-agim-skills-BD1AIi8d.js.map} +1 -1
- package/dist/web/public/assets/{use-background-tasks-B1Pxq3JU.js → use-background-tasks-FuodO-_C.js} +2 -2
- package/dist/web/public/assets/{use-background-tasks-B1Pxq3JU.js.map → use-background-tasks-FuodO-_C.js.map} +1 -1
- package/dist/web/public/assets/{use-memory-w-EY79Zm.js → use-memory-4xOyJX4L.js} +2 -2
- package/dist/web/public/assets/{use-memory-w-EY79Zm.js.map → use-memory-4xOyJX4L.js.map} +1 -1
- package/dist/web/public/assets/{use-observability-Cd2dUHb7.js → use-observability-DMebP5-1.js} +2 -2
- package/dist/web/public/assets/{use-observability-Cd2dUHb7.js.map → use-observability-DMebP5-1.js.map} +1 -1
- package/dist/web/public/assets/{use-settings-DG1rm9KU.js → use-settings-GPajsGBB.js} +2 -2
- package/dist/web/public/assets/{use-settings-DG1rm9KU.js.map → use-settings-GPajsGBB.js.map} +1 -1
- package/dist/web/public/assets/{use-workspace-DvW_xOmj.js → use-workspace-C6qix-sY.js} +2 -2
- package/dist/web/public/assets/{use-workspace-DvW_xOmj.js.map → use-workspace-C6qix-sY.js.map} +1 -1
- package/dist/web/public/assets/{vector-CDJ4p6UI.js → vector-hGc2RFNQ.js} +2 -2
- package/dist/web/public/assets/{vector-CDJ4p6UI.js.map → vector-hGc2RFNQ.js.map} +1 -1
- package/dist/web/public/assets/{viewer-Bje5-HtQ.js → viewer-YkLMRkmd.js} +2 -2
- package/dist/web/public/assets/{viewer-Bje5-HtQ.js.map → viewer-YkLMRkmd.js.map} +1 -1
- package/dist/web/public/assets/{workspace-B7QYGLix.js → workspace-0GRMLd5j.js} +2 -2
- package/dist/web/public/assets/{workspace-B7QYGLix.js.map → workspace-0GRMLd5j.js.map} +1 -1
- package/dist/web/public/assets/{workspaces-BlodF0DP.js → workspaces-DPa7ByRm.js} +2 -2
- package/dist/web/public/assets/{workspaces-BlodF0DP.js.map → workspaces-DPa7ByRm.js.map} +1 -1
- package/dist/web/public/index.html +2 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +4 -2
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/assets/index-BIBWEKxu.js.map +0 -1
|
@@ -1,144 +1,36 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Shared runtime pieces for Agim Agent.
|
|
2
2
|
//
|
|
3
|
-
// The native
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// ↓
|
|
13
|
-
// AgentAdapter.sendPrompt(sessionId, prompt, history, opts)
|
|
14
|
-
// ↓
|
|
15
|
-
// 1. resolveProvider(role) ← pick the LLM (role-based)
|
|
16
|
-
// 2. build LlmMessage[] from history+prompt
|
|
17
|
-
// 3. assemble dispatcher
|
|
18
|
-
// = combineDispatchers(builtin, mcp)
|
|
19
|
-
// 4. assemble approval gate (from operator policy config)
|
|
20
|
-
// 5. runAgentLoop(...) — multi-iter tool loop
|
|
21
|
-
// 6. yield the final text as a SINGLE chunk (buffered) back to the
|
|
22
|
-
// adapter's AsyncGenerator contract
|
|
23
|
-
//
|
|
24
|
-
// Stage 2 sub-PR #4 deliberately ships:
|
|
25
|
-
// - one LLM call per IM turn (no streaming of incremental thinking
|
|
26
|
-
// to IM; that's a separate feature)
|
|
27
|
-
// - sequential tool execution within each iteration (matches
|
|
28
|
-
// agent-loop's current contract)
|
|
29
|
-
// - policy-based approval (mode=allow-list by default; operators
|
|
30
|
-
// opt into looser modes via env)
|
|
31
|
-
// - the existing tryIntrospect-style usage / cost audit accounting
|
|
32
|
-
//
|
|
33
|
-
// Out of scope here (covered later):
|
|
34
|
-
// - IM-interactive approval cards (button taps / y-n IM reply); the
|
|
35
|
-
// policy gate is sufficient for the initial release
|
|
36
|
-
// - Streaming partial responses to IM (we already accumulate the
|
|
37
|
-
// final text; partial streaming wants viewer-routing support too)
|
|
38
|
-
// - Session persistence specific to native (we reuse the caller-
|
|
39
|
-
// supplied history; agim's regular session manager handles
|
|
40
|
-
// persistence across turns)
|
|
41
|
-
import { logger as rootLogger } from '../../../core/logger.js';
|
|
42
|
-
import { logInvocation } from '../../../core/audit-log.js';
|
|
43
|
-
import { runAgentLoop, getAnyProvider, getProvider, getProviderByName, listProviders, } from '../../../core/llm/index.js';
|
|
44
|
-
import { buildPolicyApprovalGate, describePolicy, } from '../../../core/llm/policy-approval-gate.js';
|
|
45
|
-
// Tool assembly (defs + dispatch + per-call concurrency classifier) moved
|
|
46
|
-
// into tool-registry.ts (#86); the per-dispatcher builder imports live there
|
|
47
|
-
// now. MAX_INJECTED_SKILLS is still needed here for the system-prompt skills
|
|
48
|
-
// cap (#85/T3).
|
|
49
|
-
import { assembleNativeTools } from './tool-registry.js';
|
|
50
|
-
import { listSkills, MAX_INJECTED_SKILLS } from '../../../core/skills/loader.js';
|
|
3
|
+
// The old in-process native AgentAdapter has been retired. Agim Agent is now
|
|
4
|
+
// implemented by the pi-agent-core backed adapter, registered
|
|
5
|
+
// under the public `native` / `agim` names. This module keeps only the shared
|
|
6
|
+
// prompt, policy, operator-role, and media helpers used by that single adapter.
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { existsSync as fsExistsSync, readFileSync as fsReadFileSync, statSync as fsStatSync } from 'node:fs';
|
|
9
|
+
import { join as pathJoin, resolve as pathResolve, sep as pathSep } from 'node:path';
|
|
10
|
+
import { approvalBus } from '../../../core/approval-bus.js';
|
|
11
|
+
import { getAnyProvider, getProvider } from '../../../core/llm/index.js';
|
|
51
12
|
import { describeRegistry as describeMcpRegistry } from '../../../core/llm/mcp-registry.js';
|
|
52
|
-
import {
|
|
53
|
-
import { handlePushOp } from '../../../core/push-rpc.js';
|
|
54
|
-
import { approvalBus, threadKey as makeThreadKey } from '../../../core/approval-bus.js';
|
|
13
|
+
import { describePolicy, } from '../../../core/llm/policy-approval-gate.js';
|
|
55
14
|
import { effectivePlanModeOn } from '../../../core/plan-mode-state.js';
|
|
56
|
-
import { randomUUID as _nativeRandomUUID } from 'node:crypto';
|
|
57
|
-
import { maybeCompactHistory } from '../../../core/llm/auto-compact.js';
|
|
58
|
-
import { existsSync as fsExistsSync, statSync as fsStatSync, readFileSync as fsReadFileSync } from 'node:fs';
|
|
59
|
-
import { resolve as pathResolve, sep as pathSep, join as pathJoin } from 'node:path';
|
|
60
15
|
import { sanitizeForInjection, scanForInjectionAttempts } from '../../../core/prompt-injection-guard.js';
|
|
16
|
+
import { buildSkillsSummary } from '../../../core/skills/loader.js';
|
|
17
|
+
import { logger as rootLogger } from '../../../core/logger.js';
|
|
61
18
|
const log = rootLogger.child({ component: 'native-agent' });
|
|
62
|
-
/**
|
|
63
|
-
* v1.2.147 — framework-level tool-call discipline injected into EVERY
|
|
64
|
-
* native turn's system prompt. Sits at the top, before the operator
|
|
65
|
-
* role definition, so it dominates any persona-level rules the user
|
|
66
|
-
* authors. Pure prompt — paired with the runtime hallucination
|
|
67
|
-
* detector (`detectHallucinatedToolCall`) that catches the failure
|
|
68
|
-
* mode if the model ignores the rule.
|
|
69
|
-
*
|
|
70
|
-
* Why this is hard-coded, not optional:
|
|
71
|
-
* - The failure (model narrates a tool call without emitting it) is
|
|
72
|
-
* LLM-side and silent; we cannot fully prevent it. But almost every
|
|
73
|
-
* instance we've seen would have been deterred by an explicit
|
|
74
|
-
* "do not narrate, just emit" rule.
|
|
75
|
-
* - Leaving this to operator AGENTS.md means every fresh install
|
|
76
|
-
* reproduces the same harm before the operator notices.
|
|
77
|
-
* - Escape hatch: IMHUB_NATIVE_TOOL_DISCIPLINE=off (or 0/false/no/
|
|
78
|
-
* disable) lets advanced operators drop the block, e.g. when
|
|
79
|
-
* measuring whether the prompt itself is hurting tool-call recall.
|
|
80
|
-
*/
|
|
81
19
|
const TOOL_CALL_DISCIPLINE_PROMPT = [
|
|
82
20
|
'## 工具调用纪律(agim 框架级硬约束)',
|
|
83
21
|
'',
|
|
84
|
-
'- 禁止用文本"描述/演练"
|
|
85
|
-
'- 工具调用必须真的发生:如果当轮没真正发出
|
|
86
|
-
'-
|
|
87
|
-
'- 用户追问"做了吗 / 写好了吗 / 又没消息了",先回顾本轮 `toolCalls` 历史;没有就直接坦白"没调成",禁止编造"这次直接写""不废话了"等空承诺——空承诺没有任何代价但会摧毁信任。',
|
|
22
|
+
'- 禁止用文本"描述/演练"工具调用;想用工具就直接发 toolCalls,不要先用纯文本预告。',
|
|
23
|
+
'- 工具调用必须真的发生:如果当轮没真正发出 toolCalls,就不要输出"已经写好 / 已经存好 / 已经调了 X"这类口径,不要假装调用了又没调。',
|
|
24
|
+
'- 用户追问"做了吗 / 写好了吗 / 又没消息了",先回顾本轮 toolCalls 历史;没有就直接坦白"没调成",禁止编造。',
|
|
88
25
|
'- write / edit / exec 类副作用工具一次性写完整调用,不要分两步"先承诺再调用"。',
|
|
89
|
-
'',
|
|
90
|
-
'(运行时会有 hallucination 检测器在末轮抓"narrate without emit",触发会用复盘卡替换原回复。)',
|
|
91
26
|
].join('\n');
|
|
92
|
-
/**
|
|
93
|
-
* Read the IMHUB_NATIVE_TOOL_DISCIPLINE kill-switch. Default ON.
|
|
94
|
-
* Recognized OFF values: 'off' / '0' / 'false' / 'no' / 'disable'.
|
|
95
|
-
* Pairs with isHallucinationDetectorOn (hallucination-detector.ts): the
|
|
96
|
-
* detector still fires when discipline is off, by design — discipline
|
|
97
|
-
* removed is a prompting test, not a license to ship lies.
|
|
98
|
-
*/
|
|
99
27
|
export function isToolDisciplineOn() {
|
|
100
28
|
const raw = (process.env.IMHUB_NATIVE_TOOL_DISCIPLINE ?? '').toLowerCase().trim();
|
|
101
|
-
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
return true;
|
|
29
|
+
return !(raw === 'off' || raw === '0' || raw === 'false' || raw === 'no' || raw === 'disable');
|
|
105
30
|
}
|
|
106
|
-
/**
|
|
107
|
-
* v1.2.47 — system prompt is rebuilt per IM turn so the model sees:
|
|
108
|
-
* - which LLM backend + role it's actually running on
|
|
109
|
-
* - the agim process working directory (no per-thread cwd today)
|
|
110
|
-
* - the live skill roster (names + one-line descriptions), with the
|
|
111
|
-
* hint to call mcp__imhub__read_skill for full bodies
|
|
112
|
-
* - external MCP servers currently connected
|
|
113
|
-
*
|
|
114
|
-
* Before v1.2.47 the prompt was a 4-line generic string; users
|
|
115
|
-
* complained that asking "what model are you" / "what skills do you
|
|
116
|
-
* have" got non-answers ("I don't know; ask the operator"). The
|
|
117
|
-
* builder closes that information gap without leaking secrets — every
|
|
118
|
-
* field surfaced here is operator-configured and non-sensitive.
|
|
119
|
-
*/
|
|
120
31
|
export function buildSystemPrompt(provider, role, cwd, threadKey) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// the COUNT to MAX_INJECTED_SKILLS (the same ceiling buildSkillsSummary
|
|
124
|
-
// uses) so a large catalog can't blow the per-turn system-prompt budget.
|
|
125
|
-
// Sorted for stable ordering; overflow collapses into a "+N more" hint
|
|
126
|
-
// that points the model at the on-demand read_skill / /skill list path.
|
|
127
|
-
// (Previously this re-rolled an UNCAPPED `name: desc` line per skill.)
|
|
128
|
-
const allSkills = listSkills().slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
129
|
-
const visibleSkills = allSkills.slice(0, MAX_INJECTED_SKILLS);
|
|
130
|
-
const skillsBlock = visibleSkills.length
|
|
131
|
-
? visibleSkills
|
|
132
|
-
.map((s) => {
|
|
133
|
-
const desc = (s.description || '').trim() || '(no description; read body via mcp__imhub__read_skill)';
|
|
134
|
-
const mark = s.available ? '' : ` (unavailable: ${s.unavailableReason ?? 'requires not met'})`;
|
|
135
|
-
return ` - ${s.name}: ${desc}${mark}`;
|
|
136
|
-
})
|
|
137
|
-
.join('\n')
|
|
138
|
-
+ (allSkills.length > visibleSkills.length
|
|
139
|
-
? `\n … and ${allSkills.length - visibleSkills.length} more (use /skill list; read any with mcp__imhub__read_skill)`
|
|
140
|
-
: '')
|
|
141
|
-
: ' (no skill cards loaded; see docs/skills.md to add one)';
|
|
32
|
+
const skillsBlock = buildSkillsSummary().trim()
|
|
33
|
+
|| '(no skill cards loaded; see docs/skills.md to add one)';
|
|
142
34
|
const mcpReg = describeMcpRegistry();
|
|
143
35
|
const externalMcp = mcpReg.servers.length
|
|
144
36
|
? mcpReg.servers
|
|
@@ -146,21 +38,12 @@ export function buildSystemPrompt(provider, role, cwd, threadKey) {
|
|
|
146
38
|
.join('\n')
|
|
147
39
|
: ' (none configured)';
|
|
148
40
|
const lines = [];
|
|
149
|
-
// v1.2.147 — framework-level tool-call discipline. Prepended BEFORE
|
|
150
|
-
// the operator role so it dominates persona-level rules. Hard-coded
|
|
151
|
-
// (not derived from a file) so every fresh agim install gets it, no
|
|
152
|
-
// per-user setup. Pairs with the runtime hallucination detector.
|
|
153
41
|
if (isToolDisciplineOn()) {
|
|
154
42
|
lines.push('[agim framework rule — tool-call discipline]');
|
|
155
43
|
lines.push(TOOL_CALL_DISCIPLINE_PROMPT);
|
|
156
44
|
lines.push('[/agim framework rule]');
|
|
157
45
|
lines.push('');
|
|
158
46
|
}
|
|
159
|
-
// Operator-supplied role definition. Reads <cwd>/AGENTS.md (seeded by
|
|
160
|
-
// bootstrapAgentWorkspaces) and prepends it as a role block. Lets
|
|
161
|
-
// operators customise the agent's identity, tone, and house rules
|
|
162
|
-
// without touching code. Sanitised + scanned for prompt-injection
|
|
163
|
-
// patterns (best-effort warn; never blocks turn).
|
|
164
47
|
const opRole = readOperatorRole(cwd);
|
|
165
48
|
if (opRole) {
|
|
166
49
|
lines.push('[operator role definition]');
|
|
@@ -168,44 +51,37 @@ export function buildSystemPrompt(provider, role, cwd, threadKey) {
|
|
|
168
51
|
lines.push('[/operator role definition]');
|
|
169
52
|
lines.push('');
|
|
170
53
|
}
|
|
171
|
-
lines.push(`You are agim native — agim's in-process LLM agent talking to a user over an IM platform.`,
|
|
172
|
-
// v1.2.119 — PlanMode banner. When the env knob is on, prepend a
|
|
173
|
-
// high-priority instruction block so the model knows up-front it
|
|
174
|
-
// can't write/exec. Without this banner, the deny verdict from
|
|
175
|
-
// the policy gate gives a bare "tool call denied" message and the
|
|
176
|
-
// model wastes iterations trying alternative write paths.
|
|
54
|
+
lines.push(`You are agim native — agim's in-process LLM agent talking to a user over an IM platform.`, '', 'Runtime:', ` Backend: ${provider.providerType}:${provider.name}`, ` Role: ${role}`, ` Working directory: ${cwd}`, '');
|
|
177
55
|
if (isPlanModeOn(threadKey)) {
|
|
178
|
-
lines.push(
|
|
56
|
+
lines.push('⚠ Plan mode is ACTIVE', ' - You MUST produce a read-only plan; native_write_file and native_exec are HARD-BLOCKED.', ' - Use read tools freely: native_read_file / native_list_dir / native_glob / native_grep / native_web_fetch / native_web_search.', ' - When the plan is ready, call native_exit_plan_mode({ plan }) so the user can approve or reject it.', '');
|
|
179
57
|
}
|
|
180
|
-
lines.push(`Tools available beyond the four native built-ins (echo / now / sleep / random_uuid):`, ` - agim built-in MCP tools (mcp__imhub__*): read_skill, list_skills, save_memo, search_memos, update_memo, delete_memo, push_message, ask_user, call_agent, long_task, complete_goal`, ` - native filesystem tools: native_read_file, native_write_file, native_list_dir, native_glob, native_grep — constrained to your workspace cwd unless IMHUB_NATIVE_FS_RESTRICT=0`, ` - native web tools: native_web_fetch (r.jina.ai reader by default), native_web_search (duckduckgo → metaso fallback). Private IPs blocked.`, ` - native_exec(command, timeout_ms?, cwd?): run shell commands. Always approval-gated; bwrap sandbox when IMHUB_EXEC_SANDBOX=bwrap.`, ` - External MCP servers configured by the operator:`, externalMcp, ``, `Available skill cards (call mcp__imhub__read_skill('<name>') for the full body):`, skillsBlock, ``, `Guidance:`, ` - Be terse; avoid filler. Prefer tool use over guessing.`, ` - When uncertain, call mcp__imhub__ask_user(question, choices[]) instead of free-form back-and-forth.`, ` - When the user references something they told the bot before, search memos via mcp__imhub__search_memos.`, ``, `Tool selection priority (HARD RULE — v1.2.59):`, ` - For "read this file / list this dir / search this content / fetch this URL", you MUST FIRST try`, ` your own native tools: native_read_file, native_list_dir, native_glob, native_grep,`, ` native_web_fetch (if available). Do NOT delegate these to call_agent.`, ` - call_agent is reserved for tasks that genuinely need a CLI agent's specialised capabilities`, ` (writing/editing source code in a real repo → claude-code; long-running plans → codex; etc).`, ` - You have a per-turn call_agent cap (default 2). Burning it on file reads will leave you`, ` unable to delegate later when you actually need to.`, ``, `When to USE call_agent (v1.2.139 positive framing — pair with the HARD RULE above):`, ` - The task needs sustained source-code editing in a real repo → call_agent('claude-code', …)`, ` or call_agent('codex', …). Don't try to mimic them with native_write_file when the work is`, ` multi-file refactors or feature builds.`, ` - You need a second pair of eyes / cross-checking on your own conclusion → call_agent('codex',`, ` 'audit my findings: …'). Useful before committing or reporting.`, ` - The task is large enough that parallel research helps (e.g. survey 3 independent areas of`, ` a codebase) → fan-out via call_agent('native', …) so each sub-agent's context is fresh.`, ` - Tip: write the sub-agent prompt as a self-contained brief — they don't see this conversation.`, ``, `Verification subagent (T5 — harness pattern, the most reliably useful multi-agent move):`, ` - After you produce a SUBSTANTIVE result (a multi-step conclusion, a refactor, a data`, ` analysis, anything you're about to report or commit), spin up a FRESH subagent whose sole`, ` job is to verify it independently:`, ` call_agent('codex', 'Verify the result below against <source / acceptance criteria>.`, ` Report ONLY discrepancies or risks; if it checks out, say so. <paste result>')`, ` Use 'codex' for code/logic review, 'native' for a fresh-eyes fact/consistency check.`, ` - Give the verifier a SELF-CONTAINED brief: restate the claim AND how to check it, and paste`, ` the artifact. It does NOT see this conversation — "verify my findings" with nothing attached`, ` is useless.`, ` - Synthesize, don't delegate understanding: digest what you learned into a PRECISE spec before`, ` delegating implementation or verification. "Based on your findings, fix it" is an anti-pattern`, ` — you (the coordinator) must state exactly what to do, not hand off the thinking.`, ` - Don't over-verify: skip the verifier for trivial / low-stakes turns (it costs a call_agent hop).`, ``, `Web tool routing (HARD RULE — v1.2.64):`, ` - If the user provided a SPECIFIC URL (http://… or https://…) → native_web_fetch.`, ` - If the user wants to FIND / DISCOVER something by keywords ("查找最新 X" / "search for Y" /`, ` "find docs on Z" / "今天 / 最近的 W") → native_web_search FIRST. Don't guess a URL.`, ` - Common pattern: native_web_search(query) → pick a result → native_web_fetch(that.url).`, ` - NEVER call native_web_fetch with a URL you fabricated from the user's keywords.`, ``, `Short-input rule:`, ` - If the user's message is ONLY a slash command alias for an agent name (e.g. "/agim", "/native", "/llm",`, ` "/na", "/cc", "/oc") and you are already that agent, respond with ONE short line confirming`, ` your identity (e.g. "我是 native,正在听。"). Do NOT call any tool. The slash router handles`, ` actual agent switching; if it didn't switch, the user is already on this agent.`, ``, `Plan tracking (v1.2.124 — native_todo_write):`, ` - When the user gives you a task with ≥ 3 distinct steps, FIRST call native_todo_write({items}) to`, ` write out your plan, then update statuses as you complete each step.`, ` Status values: pending | in_progress | completed. Keep exactly one item in_progress at a time.`, ` - The tool result is a rendered markdown checklist; the user sees your progress.`, ` - Don't call native_todo_write for trivial one-step tasks — overhead.`, ` - Example sequence:`, ` 1) native_todo_write([{c:"Fetch market data",s:"in_progress"}, {c:"Analyse",s:"pending"}, {c:"Reply",s:"pending"}])`, ` 2) … do fetch via native_web_fetch …`, ` 3) native_todo_write([{c:"Fetch market data",s:"completed"}, {c:"Analyse",s:"in_progress"}, {c:"Reply",s:"pending"}])`, ` 4) … analysis …`, ` 5) write final answer to user`, ``, `Closure rule (v1.2.94 — HARD RULE):`, ` - When you finish a tool chain (read_file / web_fetch / native_exec / search_memos / etc.) you`, ` MUST write a short Chinese summary BEFORE stopping. The tool output by itself is not a`, ` user-facing answer — the user can't see the raw JSON / shell stdout. Always close with at`, ` least one sentence stating the finding / conclusion.`, ` - Do NOT end a turn with empty assistant text when you've just called tools. If you genuinely`, ` have nothing to add, say so explicitly ("已查完,未发现 X").`, ``, `Proactive memory rule (v1.2.96 — borrowed from Hermes Agent's "agent-curated memory"):`, ` - Before ending a substantive turn, scan the conversation for facts that should outlive the`, ` current chat. Persist them yourself via mcp__imhub__save_memo — don't wait for the user to`, ` ask you to remember.`, ` - Worth saving (call save_memo for each):`, ` · personal preferences ("我不喝咖啡" / "我用 vim"),`, ` · holdings or portfolio codes ("我持有 600519"),`, ` · recurring people / places ("我家在朝阳" / "爸爸生日 5月8日"),`, ` · stable identifiers (账号 / 邮箱 / API base / 配置路径),`, ` · explicit "记一下" / "remember this" instructions.`, ` - NOT worth saving: one-off questions, transient debugging context, tool outputs that are`, ` already cached elsewhere (memos point AT data, they're not a cache of data).`, ` - Each save_memo call is cheap. Two short memos beat one long one — small atomic facts`, ` search better. Add a 1-line user-facing acknowledgement so the user knows you remembered`, ` (e.g. "已记下 600519 是你的持仓").`, ``, `Long-task SOP (v1.2.93 — for any work you estimate will run > 10 minutes):`, ` - You CANNOT keep a long synchronous turn alive: the IM bridge times out around 30 min, and`, ` most useful work past the 10-min mark loses intermediate state if it crashes mid-flight.`, ` - Instead, use native_exec to invoke the agim bgjob wrapper, which spawns a detached worker`, ` that survives independent of this conversation:`, ` native_exec("/root/.claude/scripts/bgjob start <slug> -- /usr/bin/python3 /path/to/script.py [args]")`, ` Substitute python3 for the runtime you actually need. The wrapper returns a job_id; relay it`, ` to the user verbatim and tell them how to check back: \`bgjob status <id>\` / \`bgjob tail <id> -f\`.`, ` - When the user follows up asking about the job, native_exec calls like \`bgjob status <id>\` or`, ` \`bgjob tail <id> -n 100\` give you the current state + recent log lines.`, ` - The bwrap sandbox (when configured) is bypassed for this specific wrapper path so the`, ` setsid-detached worker actually survives. Any OTHER native_exec command remains sandboxed.`, ` - DO NOT use \`nohup ... &\` or backgrounded shell pipelines for long work — those die with the`, ` parent shell. bgjob is the only correct path on this platform.`, ``, `Python-RPC bridge (v1.2.97 — when a task means MANY similar tool calls):`, ` - When you would otherwise call mcp__imhub__* dozens of times in this chat turn (saving 30`, ` facts, fetching 50 stocks, scoring 100 candidates), DO NOT do it inline — that wastes the`, ` iteration budget and is likely to trip the stuck-loop detector. Write ONE Python script,`, ` run it in bgjob, and let it loop locally while calling back to agim's tool surface via the`, ` local RPC bridge agim sets up automatically for every native_exec child.`, ` - The Python sidecar lives at \`<npm install dir>/bin/agim_rpc.py\` (typically`, ` /usr/local/lib/node_modules/agim-cli/bin/agim_rpc.py — find it with`, ` \`node -e "console.log(require.resolve('agim-cli'))"\`). Import it and instantiate the client:`, ` from agim_rpc import client`, ` rpc = client() # reads env, validates token, no args needed`, ` memos = rpc.search_memos(query="茅台", k=10)`, ` for m in memos.get("rows", []):`, ` ...`, ` rpc.push_message(text="后台跑完了,结果是 X")`, ` - Available tools through the bridge (whitelist): search_memos, save_memo, read_skill,`, ` list_skills, push_message. Everything else (native_exec, fs writes, call_agent, long_task,`, ` ask_user) is NOT exposed — the worker already has a shell + filesystem.`, ` - The token is automatically injected via env (IMHUB_RPC_SOCKET + IMHUB_RPC_TOKEN), bound`, ` to THIS IM thread, valid for 24 h. The worker can only drive this thread; it cannot`, ` push_message into someone else's chat.`, ` - End the worker with rpc.push_message(text="…done…") so the user sees the result come back`, ` asynchronously. Don't expect the user to poll \`bgjob tail\` themselves.`);
|
|
58
|
+
lines.push('Tools available beyond the four native built-ins (echo / now / sleep / random_uuid):', ' - agim built-in MCP tools (mcp__imhub__*): read_skill, list_skills, save_memo, search_memos, update_memo, delete_memo, push_message, ask_user, call_agent, long_task, complete_goal', ' - native filesystem tools: native_read_file, native_write_file, native_list_dir, native_glob, native_grep — constrained to your workspace cwd unless IMHUB_NATIVE_FS_RESTRICT=0', ' - native web tools: native_web_fetch, native_web_search. Private IPs blocked by default.', ' - native_exec(command, timeout_ms?, cwd?): run shell commands. Always approval-gated.', ' - External MCP servers configured by the operator:', externalMcp, '', 'Agim Skills system-prompt injection:', skillsBlock, '', 'Guidance:', ' - Be terse; avoid filler. Prefer tool use over guessing.', ' - When uncertain, call mcp__imhub__ask_user(question, choices[]) instead of free-form back-and-forth.', ' - When the user references something they told the bot before, search memos via mcp__imhub__search_memos.', ' - For file/list/search/fetch requests, use native tools first; reserve call_agent for work that genuinely needs another CLI agent.', ' - Closure rule: after a tool chain, write a short Chinese summary before stopping.', '', 'Verification subagent:', ' - After a substantive result, use call_agent with a fresh verifier when a second pass is valuable.', " - Synthesize, don't delegate understanding: state the exact claim and how to verify it.", ' - Give the verifier a self-contained brief; it does not see this conversation.');
|
|
181
59
|
return lines.join('\n');
|
|
182
60
|
}
|
|
183
|
-
const
|
|
61
|
+
const operatorRoleCache = new Map();
|
|
184
62
|
function readRoleFile(path) {
|
|
185
63
|
let st;
|
|
186
64
|
try {
|
|
187
65
|
st = fsStatSync(path);
|
|
188
66
|
}
|
|
189
67
|
catch {
|
|
190
|
-
|
|
191
|
-
_operatorRoleCache.delete(path);
|
|
68
|
+
operatorRoleCache.delete(path);
|
|
192
69
|
return '';
|
|
193
70
|
}
|
|
194
|
-
const cached =
|
|
195
|
-
if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size)
|
|
71
|
+
const cached = operatorRoleCache.get(path);
|
|
72
|
+
if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size)
|
|
196
73
|
return cached.content;
|
|
197
|
-
}
|
|
198
74
|
let raw = '';
|
|
199
75
|
try {
|
|
200
76
|
raw = fsReadFileSync(path, 'utf-8');
|
|
201
77
|
}
|
|
202
78
|
catch {
|
|
203
|
-
|
|
79
|
+
operatorRoleCache.delete(path);
|
|
204
80
|
return '';
|
|
205
81
|
}
|
|
206
82
|
const trimmed = raw.trim();
|
|
207
83
|
if (!trimmed) {
|
|
208
|
-
|
|
84
|
+
operatorRoleCache.set(path, { mtimeMs: st.mtimeMs, size: st.size, content: '' });
|
|
209
85
|
return '';
|
|
210
86
|
}
|
|
211
87
|
try {
|
|
@@ -220,41 +96,15 @@ function readRoleFile(path) {
|
|
|
220
96
|
}
|
|
221
97
|
catch { /* scan is best-effort */ }
|
|
222
98
|
const content = sanitizeForInjection(trimmed, 8000);
|
|
223
|
-
|
|
99
|
+
operatorRoleCache.set(path, { mtimeMs: st.mtimeMs, size: st.size, content });
|
|
224
100
|
return content;
|
|
225
101
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Resolve the operator role definition injected into native's system prompt.
|
|
228
|
-
*
|
|
229
|
-
* T4 (instruction hierarchy, distilled from the agentic-harness Memory
|
|
230
|
-
* pattern's "local overrides win — always"). Instead of a single
|
|
231
|
-
* `<cwd>/AGENTS.md`, discover a layered stack within the native workspace
|
|
232
|
-
* and concatenate in ASCENDING priority so the most-local block appears last
|
|
233
|
-
* and gets the most model attention. LOCAL WINS:
|
|
234
|
-
*
|
|
235
|
-
* 1. project — <cwd>/AGENTS.md (the shared native-workspace role)
|
|
236
|
-
* 2. local — <cwd>/AGENTS.local.md (private override, not version-ctl'd)
|
|
237
|
-
*
|
|
238
|
-
* Both layers live under the native workspace cwd, so the stack is
|
|
239
|
-
* self-contained (no dependency on a global ~/.agim file — native has a
|
|
240
|
-
* single workspace, so "project" already IS the operator-global role; a
|
|
241
|
-
* cross-workspace user layer can be added later if that changes).
|
|
242
|
-
*
|
|
243
|
-
* IMHUB_NATIVE_AGENT_ROLE_FILE still forces a single explicit file (operators
|
|
244
|
-
* who pin one path bypass discovery entirely). Each layer is independently
|
|
245
|
-
* memoized + injection-scanned + capped at 8000 chars by readRoleFile.
|
|
246
|
-
*/
|
|
247
102
|
export function readOperatorRole(cwd) {
|
|
248
103
|
const override = process.env.IMHUB_NATIVE_AGENT_ROLE_FILE;
|
|
249
|
-
if (override && override.length > 0)
|
|
250
|
-
// An explicit pin is operator-chosen → trusted regardless of the gate.
|
|
104
|
+
if (override && override.length > 0)
|
|
251
105
|
return readRoleFile(override);
|
|
252
|
-
|
|
253
|
-
// T6 — workspace trust gate. Discovered workspace files are skipped wholesale
|
|
254
|
-
// when the workspace is marked untrusted (see isWorkspaceTrusted).
|
|
255
|
-
if (!isWorkspaceTrusted()) {
|
|
106
|
+
if (!isWorkspaceTrusted())
|
|
256
107
|
return '';
|
|
257
|
-
}
|
|
258
108
|
const parts = [];
|
|
259
109
|
for (const p of [pathJoin(cwd, 'AGENTS.md'), pathJoin(cwd, 'AGENTS.local.md')]) {
|
|
260
110
|
const c = readRoleFile(p);
|
|
@@ -263,30 +113,10 @@ export function readOperatorRole(cwd) {
|
|
|
263
113
|
}
|
|
264
114
|
return parts.join('\n\n');
|
|
265
115
|
}
|
|
266
|
-
/**
|
|
267
|
-
* T6 — workspace trust gate (distilled from the agentic-harness Lifecycle
|
|
268
|
-
* pattern's "trust is all-or-nothing; an untrusted workspace disables the
|
|
269
|
-
* whole extension surface, not just suspicious parts").
|
|
270
|
-
*
|
|
271
|
-
* agim treats `<cwd>/AGENTS.md` + `AGENTS.local.md` as an operator-authored
|
|
272
|
-
* role definition injected into the system prompt — a privileged surface. In
|
|
273
|
-
* multi-tenant / A2A setups the native cwd can point at a directory whose
|
|
274
|
-
* contents are NOT fully operator-controlled, where an attacker-planted
|
|
275
|
-
* AGENTS.md is a prompt-injection vector. Setting
|
|
276
|
-
* `IMHUB_NATIVE_TRUST_WORKSPACE=off` (or 0/false/no) makes readOperatorRole
|
|
277
|
-
* skip ALL workspace-discovered role files at once. Default is trusted
|
|
278
|
-
* (on) for backward compatibility — operators opt into the stricter posture.
|
|
279
|
-
*
|
|
280
|
-
* An explicit `IMHUB_NATIVE_AGENT_ROLE_FILE` bypasses the gate: the operator
|
|
281
|
-
* pinned that exact file deliberately, so it stays trusted.
|
|
282
|
-
*/
|
|
283
116
|
export function isWorkspaceTrusted() {
|
|
284
117
|
const raw = (process.env.IMHUB_NATIVE_TRUST_WORKSPACE ?? '').toLowerCase().trim();
|
|
285
118
|
return !(raw === 'off' || raw === '0' || raw === 'false' || raw === 'no');
|
|
286
119
|
}
|
|
287
|
-
/** Role priority for picking which LLM backend powers the native chat
|
|
288
|
-
* turn. First found wins. Operators can override the role via
|
|
289
|
-
* `IMHUB_NATIVE_AGENT_ROLE`. */
|
|
290
120
|
const DEFAULT_ROLE_FALLBACK = ['native-chat', 'cheap'];
|
|
291
121
|
function resolveRole() {
|
|
292
122
|
const raw = process.env.IMHUB_NATIVE_AGENT_ROLE;
|
|
@@ -294,371 +124,6 @@ function resolveRole() {
|
|
|
294
124
|
return [raw.trim(), ...DEFAULT_ROLE_FALLBACK];
|
|
295
125
|
return DEFAULT_ROLE_FALLBACK.slice();
|
|
296
126
|
}
|
|
297
|
-
/**
|
|
298
|
-
* v1.2.91 / v1.2.92 — render a structured recap when a native turn
|
|
299
|
-
* ends without a normal "stop + text" completion. Two failure modes
|
|
300
|
-
* share the same skeleton:
|
|
301
|
-
*
|
|
302
|
-
* - `empty` : finishReason='stop' but text=''. Common cause:
|
|
303
|
-
* model finished a tool chain (search / fetch /
|
|
304
|
-
* read / ask_user) and skipped the closing summary.
|
|
305
|
-
* - `max_iter` : the loop hit IMHUB_NATIVE_AGENT_MAX_ITER without
|
|
306
|
-
* the model deciding to stop. The model wanted to
|
|
307
|
-
* keep going. We were the ones who pulled the plug.
|
|
308
|
-
*
|
|
309
|
-
* Both surface:
|
|
310
|
-
* 1. what tool calls actually ran (✓ / ✗ / ⚠️, deduped by name)
|
|
311
|
-
* 2. a 160-char preview of the last tool's output
|
|
312
|
-
* 3. a plain-language guess at why we're here
|
|
313
|
-
* 4. concrete continuation options
|
|
314
|
-
*
|
|
315
|
-
* Pure formatting of AgentLoopResult fields; no second LLM call.
|
|
316
|
-
*/
|
|
317
|
-
function composeUnfinishedTurnRecap(result, kind, maxIter) {
|
|
318
|
-
const tools = result.toolCalls;
|
|
319
|
-
if (tools.length === 0) {
|
|
320
|
-
// No tools called AND no text emitted — the model literally said
|
|
321
|
-
// nothing. Usually a misjudged "nothing to do" or a provider
|
|
322
|
-
// quirk on a single short prompt. (max_iter with 0 tools is
|
|
323
|
-
// unusual but possible if the model returned empty assistant
|
|
324
|
-
// text every iteration; treat the same way.)
|
|
325
|
-
return [
|
|
326
|
-
'🧐 这一轮没说话也没动工具。',
|
|
327
|
-
'可能是模型把请求误判成了"无事可做",或者提供方返回了空响应。',
|
|
328
|
-
'',
|
|
329
|
-
'怎么继续:',
|
|
330
|
-
' · 直接告诉我具体要做什么(多给点上下文)',
|
|
331
|
-
' · 或把上一条请求换种说法再发',
|
|
332
|
-
' · 或 /cc / /oc / /cs 切到别的智能体接手',
|
|
333
|
-
].join('\n');
|
|
334
|
-
}
|
|
335
|
-
// Group identical tool names so a chain like 6× read_file collapses
|
|
336
|
-
// into "read_file ×6" rather than 6 list items.
|
|
337
|
-
const counts = new Map();
|
|
338
|
-
for (const t of tools) {
|
|
339
|
-
const e = counts.get(t.name);
|
|
340
|
-
if (e) {
|
|
341
|
-
e.count += 1;
|
|
342
|
-
if (t.isError)
|
|
343
|
-
e.errors += 1;
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
counts.set(t.name, { count: 1, errors: t.isError ? 1 : 0 });
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
const intro = kind === 'stuck_loop'
|
|
350
|
-
? `🛑 检测到死循环:模型在第 ${result.iterations} 步连续 3 次调用同一个工具拿到完全一样的结果,已提前停下。已执行的工具调用:`
|
|
351
|
-
: kind === 'max_iter'
|
|
352
|
-
? `⚠️ 这一轮干到第 ${result.iterations} 步还没收尾,被安全上限切掉了。已执行的工具调用:`
|
|
353
|
-
: '🧐 这一轮没写出收尾文字就结束了,但中间有做事。已执行的工具调用:';
|
|
354
|
-
const lines = [intro];
|
|
355
|
-
for (const [name, { count, errors }] of counts) {
|
|
356
|
-
const mark = errors === 0 ? '✓' : errors === count ? '✗' : '⚠️';
|
|
357
|
-
const tail = count > 1 ? ` ×${count}` : '';
|
|
358
|
-
const errTail = errors > 0 && errors < count ? `(其中 ${errors} 次失败)` : '';
|
|
359
|
-
lines.push(` ${mark} ${name}${tail}${errTail}`);
|
|
360
|
-
}
|
|
361
|
-
const last = tools[tools.length - 1];
|
|
362
|
-
if (last) {
|
|
363
|
-
const preview = (last.preview ?? '').trim().slice(0, 160).replace(/\s+/g, ' ');
|
|
364
|
-
lines.push('');
|
|
365
|
-
lines.push(`最后一步:${last.name}${last.isError ? '(失败)' : ''}`);
|
|
366
|
-
if (preview)
|
|
367
|
-
lines.push(` └ 结果摘要:${preview}${(last.preview?.length ?? 0) > 160 ? '…' : ''}`);
|
|
368
|
-
}
|
|
369
|
-
// Quick why-did-it-stop hint based on what we have.
|
|
370
|
-
lines.push('');
|
|
371
|
-
if (kind === 'stuck_loop' && last) {
|
|
372
|
-
lines.push(`判定:${last.name} 工具连续 3 次返回了完全一样的内容(${last.isError ? '同一个错误' : '同一份输出'}),` +
|
|
373
|
-
`继续重试不会带来新结果。请换个写法 / 换个参数 / 换个工具,或者把任务拆小。`);
|
|
374
|
-
}
|
|
375
|
-
else if (kind === 'max_iter') {
|
|
376
|
-
lines.push(`猜测:模型还想继续,但跑到第 ${result.iterations} 步触发了安全上限(IMHUB_NATIVE_AGENT_MAX_ITER=${maxIter})。任务规模偏大或卡在某一步反复重试。`);
|
|
377
|
-
}
|
|
378
|
-
else if (last && !last.isError) {
|
|
379
|
-
lines.push('猜测:模型在工具结果上停手了,没写最终结论。常见原因是它把工具的返回当成了"完整答案"。');
|
|
380
|
-
}
|
|
381
|
-
else if (last && last.isError) {
|
|
382
|
-
lines.push('猜测:最后一步工具失败了,模型没生成补救/解释文字就停了。');
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
lines.push(`猜测:模型在第 ${result.iterations} 轮结束时主动停止(finishReason=${result.finishReason})。`);
|
|
386
|
-
}
|
|
387
|
-
lines.push('');
|
|
388
|
-
if (kind === 'stuck_loop') {
|
|
389
|
-
lines.push('怎么继续:');
|
|
390
|
-
lines.push(' · 告诉我换什么写法 / 换什么参数(最有用)');
|
|
391
|
-
lines.push(' · 或把任务拆小一些(先解决卡住的那一步)');
|
|
392
|
-
lines.push(' · 或 /cc / /oc / /cs 切到别的智能体接手');
|
|
393
|
-
}
|
|
394
|
-
else if (kind === 'max_iter') {
|
|
395
|
-
lines.push('怎么继续:');
|
|
396
|
-
lines.push(' · 回「继续」让我接着把剩下的事做完');
|
|
397
|
-
lines.push(' · 或把任务拆小一些(先做 A,再做 B)');
|
|
398
|
-
lines.push(` · 或调高上限:在 .agim/env 加一行 IMHUB_NATIVE_AGENT_MAX_ITER=${Math.min(maxIter * 2, 100)} 后重启服务`);
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
lines.push('要继续吗?');
|
|
402
|
-
lines.push(' · 回「继续」让我接着推进');
|
|
403
|
-
lines.push(' · 或直接告诉我下一步具体做什么');
|
|
404
|
-
lines.push(' · 或 /cc / /oc / /cs 切到别的智能体接手');
|
|
405
|
-
}
|
|
406
|
-
return lines.join('\n');
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* v1.2.98 — render the off-track recap from the goal-critic's verdict.
|
|
410
|
-
* Different shape from `composeUnfinishedTurnRecap` because the critic
|
|
411
|
-
* gave us a verbatim Chinese reason + optional redirect; we don't have
|
|
412
|
-
* to invent fail-mode hints. Layout:
|
|
413
|
-
*
|
|
414
|
-
* 🧭 目标偏离检测:模型偏离了原目标。
|
|
415
|
-
* 原因:<critic.reason>
|
|
416
|
-
* 建议方向:<critic.redirect> (only if non-empty)
|
|
417
|
-
*
|
|
418
|
-
* 已执行的工具调用:
|
|
419
|
-
* ✓ tool_a ×2
|
|
420
|
-
* ✗ tool_b ×3 (失败)
|
|
421
|
-
*
|
|
422
|
-
* 要继续吗?
|
|
423
|
-
* · 回「继续」按原方向接着推
|
|
424
|
-
* · 回「换」按上面的建议方向走
|
|
425
|
-
* · 或自己说一个新方向 / 切到别的 agent
|
|
426
|
-
*/
|
|
427
|
-
function composeOffTrackRecap(result, reason, redirect) {
|
|
428
|
-
const lines = [];
|
|
429
|
-
lines.push('🧭 目标偏离检测:模型在原目标上不再有进展。');
|
|
430
|
-
if (reason)
|
|
431
|
-
lines.push(`原因:${reason}`);
|
|
432
|
-
if (redirect)
|
|
433
|
-
lines.push(`建议方向:${redirect}`);
|
|
434
|
-
lines.push('');
|
|
435
|
-
if (result.toolCalls.length > 0) {
|
|
436
|
-
lines.push(`已执行的工具调用(共 ${result.toolCalls.length} 次):`);
|
|
437
|
-
const counts = new Map();
|
|
438
|
-
for (const t of result.toolCalls) {
|
|
439
|
-
const e = counts.get(t.name);
|
|
440
|
-
if (e) {
|
|
441
|
-
e.count += 1;
|
|
442
|
-
if (t.isError)
|
|
443
|
-
e.errors += 1;
|
|
444
|
-
}
|
|
445
|
-
else
|
|
446
|
-
counts.set(t.name, { count: 1, errors: t.isError ? 1 : 0 });
|
|
447
|
-
}
|
|
448
|
-
for (const [name, { count, errors }] of counts) {
|
|
449
|
-
const mark = errors === 0 ? '✓' : errors === count ? '✗' : '⚠️';
|
|
450
|
-
const tail = count > 1 ? ` ×${count}` : '';
|
|
451
|
-
const errTail = errors > 0 && errors < count ? `(其中 ${errors} 次失败)` : '';
|
|
452
|
-
lines.push(` ${mark} ${name}${tail}${errTail}`);
|
|
453
|
-
}
|
|
454
|
-
lines.push('');
|
|
455
|
-
}
|
|
456
|
-
lines.push('要继续吗?');
|
|
457
|
-
if (redirect) {
|
|
458
|
-
lines.push(' · 回「继续」按原方向接着推(已经被判定走不通)');
|
|
459
|
-
lines.push(' · 回「换」按上面的建议方向走(推荐)');
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
lines.push(' · 回「继续」按原方向接着推');
|
|
463
|
-
}
|
|
464
|
-
lines.push(' · 或自己告诉我一个新方向');
|
|
465
|
-
lines.push(' · 或 /cc / /oc / /cs 切到别的智能体接手');
|
|
466
|
-
return lines.join('\n');
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* v1.2.147 — recap for the "hallucinated tool-call" failure mode.
|
|
470
|
-
*
|
|
471
|
-
* Trigger: agent-loop detected the model's final text narrates a
|
|
472
|
-
* native_* tool invocation (e.g. "我现在调用 native_write_file:
|
|
473
|
-
* ```python ...```" or "let me invoke native_exec") but the response
|
|
474
|
-
* carried zero real toolCalls. The model promised an action it did not
|
|
475
|
-
* take. Without this branch the lie would get shipped to the user as a
|
|
476
|
-
* normal reply.
|
|
477
|
-
*
|
|
478
|
-
* Distinct from the empty / max-iter / stuck-loop recaps because the
|
|
479
|
-
* fail-mode is model-side rather than budget-side: the model is fine,
|
|
480
|
-
* just not in a tool-calling mood. The recap points the user at a
|
|
481
|
-
* backend swap as the most reliable next step, since prompt tweaking
|
|
482
|
-
* has limited leverage when the model has already drifted out of the
|
|
483
|
-
* function-calling schema. Pure formatting; no LLM call.
|
|
484
|
-
*/
|
|
485
|
-
function composeHallucinatedToolRecap(result, backend) {
|
|
486
|
-
const lines = [];
|
|
487
|
-
lines.push('🧐 模型说要调用工具,但实际上没真的发起调用。');
|
|
488
|
-
lines.push('为了不让你看到空承诺,已经把这次回复挡下。');
|
|
489
|
-
lines.push('');
|
|
490
|
-
lines.push(`当前 native-chat 后端:${backend}`);
|
|
491
|
-
if (result.toolCalls.length > 0) {
|
|
492
|
-
lines.push(`本轮在此之前已真正完成了 ${result.toolCalls.length} 次工具调用,那部分有效。`);
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
lines.push('本轮没有任何真实工具调用产出。');
|
|
496
|
-
}
|
|
497
|
-
lines.push('');
|
|
498
|
-
lines.push('怎么继续:');
|
|
499
|
-
lines.push(' · 直接回「重试」让我重新跑一遍这一步');
|
|
500
|
-
lines.push(' · 或在 /settings/llm 切换到工具调用更稳定的后端(可选再把 native-chat 角色绑定到它)');
|
|
501
|
-
lines.push(' · 或 /cc / /oc / /cs 切到别的智能体接手这一步');
|
|
502
|
-
lines.push('');
|
|
503
|
-
lines.push('(检测器可用 IMHUB_NATIVE_HALLUCINATION_DETECT=off 关掉)');
|
|
504
|
-
return lines.join('\n');
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* v1.2.142 — Stage-report retry. Replaces v1.2.94's empty-only auto-
|
|
508
|
-
* summary with a single helper used by all four "unhappy ending"
|
|
509
|
-
* branches (empty / max_iter / stuck_loop / off_track).
|
|
510
|
-
*
|
|
511
|
-
* The retry asks the same provider, with no tools, for a *user-facing
|
|
512
|
-
* stage report* given the work already done — not a "final answer". The
|
|
513
|
-
* prompt explicitly tolerates partial / failed work; failed tool
|
|
514
|
-
* results get described in plain language ("couldn't fetch the news
|
|
515
|
-
* page — anti-bot"), never as `tool_name ×N`.
|
|
516
|
-
*
|
|
517
|
-
* Returns the produced text + cost delta on success; null when the
|
|
518
|
-
* provider returned nothing or threw. Caller falls back to the
|
|
519
|
-
* technical `composeUnfinishedTurnRecap` recap on null.
|
|
520
|
-
*
|
|
521
|
-
* One LLM call, 60s deadline, tools=[] so it can't keep chaining.
|
|
522
|
-
* Env IMHUB_NATIVE_STAGE_REPORT=off disables the retry entirely
|
|
523
|
-
* (caller goes straight to the technical recap) — useful for debug.
|
|
524
|
-
*/
|
|
525
|
-
export async function tryStageReport(opts) {
|
|
526
|
-
if (isStageReportDisabled())
|
|
527
|
-
return null;
|
|
528
|
-
const recent = opts.result.toolCalls.slice(-20);
|
|
529
|
-
const success = recent.filter((t) => !t.isError);
|
|
530
|
-
const failed = recent.filter((t) => t.isError);
|
|
531
|
-
const fmtPreview = (p, cap) => (p ?? '').trim().slice(0, cap).replace(/\s+/g, ' ');
|
|
532
|
-
const successBlock = success.length === 0
|
|
533
|
-
? '(无成功的中间结果)'
|
|
534
|
-
: success
|
|
535
|
-
.map((t, i) => `${i + 1}. ${fmtPreview(t.preview, 300)}${(t.preview?.length ?? 0) > 300 ? '…' : ''}`)
|
|
536
|
-
.join('\n');
|
|
537
|
-
const failedBlock = failed.length === 0
|
|
538
|
-
? '(无失败)'
|
|
539
|
-
: failed
|
|
540
|
-
.map((t, i) => `${i + 1}. ${fmtPreview(t.preview, 200)}${(t.preview?.length ?? 0) > 200 ? '…' : ''}`)
|
|
541
|
-
.join('\n');
|
|
542
|
-
const kindHint = {
|
|
543
|
-
empty: '工具调用做完了但没写收尾文字。',
|
|
544
|
-
max_iter: `执行步数达到上限(${opts.result.iterations} 步)被切断。`,
|
|
545
|
-
stuck_loop: '检测到同一工具连续重复执行(卡循环),提前停止。',
|
|
546
|
-
off_track: '目标偏离检测:当前推进方向被判定为离原任务太远。',
|
|
547
|
-
}[opts.kind];
|
|
548
|
-
const reasonHint = opts.kind === 'off_track' && opts.offTrackReason
|
|
549
|
-
? `\n[偏离原因] ${opts.offTrackReason}`
|
|
550
|
-
: '';
|
|
551
|
-
const messages = [
|
|
552
|
-
{
|
|
553
|
-
role: 'system',
|
|
554
|
-
content: '你是 agim native 智能体。用户委托你做一项任务,你做了一些工作,但当前轮次未能正常收尾。\n' +
|
|
555
|
-
'\n' +
|
|
556
|
-
'请用中文给用户一份**阶段性报告**,目的是让用户拿到尽可能多的可用结果 + 知道接下来怎么做:\n' +
|
|
557
|
-
'\n' +
|
|
558
|
-
'【已经拿到 / 做到】基于成功的中间结果,给出具体数据、事实或结论。直接写内容,不要写"我调用了 X 工具"。如果没有任何可用结果,写"暂无"。\n' +
|
|
559
|
-
'【没拿到 / 卡在哪】基于失败的中间结果,用普通中文描述卡点:例如"新浪财经页面抓不到(疑似反爬)"、"某 shell 命令未找到"、"目标 URL 超时"。**不要出现工具名(native_xxx / mcp__imhub__xxx)**。没有失败就跳过本节。\n' +
|
|
560
|
-
'【下一步建议】给用户 1-3 个具体可执行的下一步,可包括"继续"、"换数据源"、"缩小范围"、"终止"等。\n' +
|
|
561
|
-
'\n' +
|
|
562
|
-
'硬约束:\n' +
|
|
563
|
-
'- 不要再调任何工具。\n' +
|
|
564
|
-
'- 不要列工具调用清单 / 工具计数。\n' +
|
|
565
|
-
'- 不要把失败粉饰成完成。\n' +
|
|
566
|
-
'- 失败的内容里不要让用户做诊断(除非真的需要他抉择)。\n',
|
|
567
|
-
},
|
|
568
|
-
{ role: 'user', content: opts.prompt },
|
|
569
|
-
{
|
|
570
|
-
role: 'user',
|
|
571
|
-
content: `[本轮中止原因] ${kindHint}${reasonHint}\n\n` +
|
|
572
|
-
`[成功的中间结果(最近 ${success.length}/${opts.result.toolCalls.length} 次,截 300 字预览)]\n${successBlock}\n\n` +
|
|
573
|
-
`[失败的中间结果(最近 ${failed.length} 次,截 200 字错误预览)]\n${failedBlock}\n\n` +
|
|
574
|
-
`[请基于上面的真实中间结果,按系统消息里的三节结构给阶段性报告。]`,
|
|
575
|
-
},
|
|
576
|
-
];
|
|
577
|
-
try {
|
|
578
|
-
log.info({
|
|
579
|
-
event: 'native.turn.stage_report.start',
|
|
580
|
-
sessionId: opts.sessionId,
|
|
581
|
-
backend: opts.provider.name,
|
|
582
|
-
kind: opts.kind,
|
|
583
|
-
total: opts.result.toolCalls.length,
|
|
584
|
-
successCount: success.length,
|
|
585
|
-
failedCount: failed.length,
|
|
586
|
-
}, `stage report → ${opts.kind} (succ=${success.length} fail=${failed.length} total=${opts.result.toolCalls.length})`);
|
|
587
|
-
const retry = await opts.provider.chat(messages, {
|
|
588
|
-
model: opts.model,
|
|
589
|
-
timeoutMs: 60_000,
|
|
590
|
-
signal: opts.signal,
|
|
591
|
-
});
|
|
592
|
-
if (!retry.text || !retry.text.trim()) {
|
|
593
|
-
log.warn({
|
|
594
|
-
event: 'native.turn.stage_report.empty',
|
|
595
|
-
sessionId: opts.sessionId,
|
|
596
|
-
backend: opts.provider.name,
|
|
597
|
-
kind: opts.kind,
|
|
598
|
-
}, 'stage report returned empty text — falling through to recap');
|
|
599
|
-
return null;
|
|
600
|
-
}
|
|
601
|
-
log.info({
|
|
602
|
-
event: 'native.turn.stage_report.ok',
|
|
603
|
-
sessionId: opts.sessionId,
|
|
604
|
-
backend: opts.provider.name,
|
|
605
|
-
kind: opts.kind,
|
|
606
|
-
textLen: retry.text.length,
|
|
607
|
-
costUsd: retry.usage?.costUsd ?? null,
|
|
608
|
-
}, `stage report produced ${retry.text.length} chars`);
|
|
609
|
-
return {
|
|
610
|
-
text: retry.text,
|
|
611
|
-
costUsd: typeof retry.usage?.costUsd === 'number' ? retry.usage.costUsd : null,
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
catch (err) {
|
|
615
|
-
log.warn({
|
|
616
|
-
event: 'native.turn.stage_report.failed',
|
|
617
|
-
sessionId: opts.sessionId,
|
|
618
|
-
backend: opts.provider.name,
|
|
619
|
-
kind: opts.kind,
|
|
620
|
-
err: err instanceof Error ? err.message : String(err),
|
|
621
|
-
}, 'stage report threw — falling through to recap');
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
/** v1.2.142 — Operator kill switch for the stage-report retry. Defaults
|
|
626
|
-
* ON. Set `IMHUB_NATIVE_STAGE_REPORT=off` (or `0` / `no` / `false`) to
|
|
627
|
-
* skip the retry and go straight to the technical recap — debug only. */
|
|
628
|
-
function isStageReportDisabled() {
|
|
629
|
-
const raw = (process.env.IMHUB_NATIVE_STAGE_REPORT ?? '').toLowerCase().trim();
|
|
630
|
-
return raw === 'off' || raw === '0' || raw === 'no' || raw === 'false';
|
|
631
|
-
}
|
|
632
|
-
/** Read IMHUB_NATIVE_AGENT_MAX_ITER, clamp to [1, 100], default 50.
|
|
633
|
-
* v1.2.136: bumped from 20 → 50 after observing real-world CR / multi-step
|
|
634
|
-
* refactor tasks routinely needed 28-35 iters and 20 was cutting them off.
|
|
635
|
-
* 50 covers most cases with margin; the 30-min IM hard timeout is the
|
|
636
|
-
* earlier ceiling for genuinely long runs, and v1.2.122 semantic stuck-
|
|
637
|
-
* loop detection catches runaway model behaviour well before 50.
|
|
638
|
-
* v1.2.92 — previously this env var was named in the user-facing
|
|
639
|
-
* banner ("raise IMHUB_NATIVE_AGENT_MAX_ITER") but NOTHING in code
|
|
640
|
-
* read it. Now actually wired into runAgentLoop. */
|
|
641
|
-
function resolveMaxIterations() {
|
|
642
|
-
const raw = process.env.IMHUB_NATIVE_AGENT_MAX_ITER;
|
|
643
|
-
if (!raw?.trim())
|
|
644
|
-
return 50;
|
|
645
|
-
const n = parseInt(raw, 10);
|
|
646
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
647
|
-
return 50;
|
|
648
|
-
return Math.min(Math.max(n, 1), 100);
|
|
649
|
-
}
|
|
650
|
-
/** v1.2.112 — opt the native agent into the agent-loop's streaming
|
|
651
|
-
* code path. Default ON: the headline benefit is preserving partial
|
|
652
|
-
* assistant text when IM's 30-min hard timeout fires mid-response.
|
|
653
|
-
* Ops can flip via `IMHUB_NATIVE_STREAM_PARTIAL=off` if a vendor's
|
|
654
|
-
* streaming endpoint misbehaves. Note the agent-loop also honours
|
|
655
|
-
* process-wide `IMHUB_AGENT_LOOP_STREAM=off` as a global kill switch. */
|
|
656
|
-
function resolveNativeStreamPartial() {
|
|
657
|
-
const raw = (process.env.IMHUB_NATIVE_STREAM_PARTIAL ?? '').toLowerCase().trim();
|
|
658
|
-
if (raw === 'off' || raw === 'false' || raw === '0' || raw === 'no')
|
|
659
|
-
return false;
|
|
660
|
-
return true;
|
|
661
|
-
}
|
|
662
127
|
function pickProvider() {
|
|
663
128
|
for (const role of resolveRole()) {
|
|
664
129
|
const p = getProvider(role);
|
|
@@ -670,148 +135,38 @@ function pickProvider() {
|
|
|
670
135
|
return { provider: fallback, role: 'auto' };
|
|
671
136
|
return null;
|
|
672
137
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
* a registered provider — de-duplicated by provider name so we never
|
|
678
|
-
* retry the exact same backend.
|
|
679
|
-
*
|
|
680
|
-
* The loop driver in sendPrompt walks this chain when an iteration ends
|
|
681
|
-
* with a TRANSIENT provider error (HTTP 5xx / network reset / 408).
|
|
682
|
-
* Non-transient errors (4xx misconfig, schema validation) end the turn
|
|
683
|
-
* immediately — retrying won't help and just multiplies cost.
|
|
684
|
-
*/
|
|
685
|
-
function pickProviderChain() {
|
|
686
|
-
const chain = [];
|
|
687
|
-
const seen = new Set();
|
|
688
|
-
for (const role of resolveRole()) {
|
|
689
|
-
const p = getProvider(role);
|
|
690
|
-
if (!p)
|
|
691
|
-
continue;
|
|
692
|
-
if (seen.has(p.name))
|
|
693
|
-
continue;
|
|
694
|
-
seen.add(p.name);
|
|
695
|
-
chain.push({ provider: p, role });
|
|
696
|
-
}
|
|
697
|
-
// Roles are optional now. If no role resolves (or role-resolved set is
|
|
698
|
-
// incomplete), append any remaining configured backends so native can run
|
|
699
|
-
// as soon as one backend is configured.
|
|
700
|
-
for (const meta of listProviders()) {
|
|
701
|
-
const p = getProviderByName(meta.name);
|
|
702
|
-
if (!p || seen.has(p.name))
|
|
703
|
-
continue;
|
|
704
|
-
seen.add(p.name);
|
|
705
|
-
chain.push({ provider: p, role: 'auto' });
|
|
706
|
-
}
|
|
707
|
-
return chain;
|
|
138
|
+
function splitEnvList(raw) {
|
|
139
|
+
if (!raw)
|
|
140
|
+
return [];
|
|
141
|
+
return raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
708
142
|
}
|
|
709
|
-
|
|
710
|
-
* `allow-list` mode with NO allow entries — equivalent to "no tool
|
|
711
|
-
* calls". Operators that want native to actually call tools must
|
|
712
|
-
* configure at least one of these env vars:
|
|
713
|
-
*
|
|
714
|
-
* IMHUB_NATIVE_AGENT_MODE=allow-all|read-only|allow-list|deny-all
|
|
715
|
-
* IMHUB_NATIVE_AGENT_AUTOALLOW=tool1,tool2,...
|
|
716
|
-
* IMHUB_NATIVE_AGENT_DENYLIST=tool1,...
|
|
717
|
-
*
|
|
718
|
-
* The `read-only` mode is the recommended starter — it accepts every
|
|
719
|
-
* tool whose name starts/ends with a read-only verb (`read_*` /
|
|
720
|
-
* `list_*` / `get_*` / etc.) and the four built-ins
|
|
721
|
-
* (`native_echo`/`now`/`random_uuid` — `sleep_ms` excluded).
|
|
722
|
-
*/
|
|
143
|
+
const PLAN_MODE_DENY_TOOLS = ['native_write_file', 'native_exec'];
|
|
723
144
|
export function resolvePolicy(threadKey) {
|
|
724
145
|
const mode = (process.env.IMHUB_NATIVE_AGENT_MODE || 'allow-list').toLowerCase();
|
|
725
146
|
const autoAllow = splitEnvList(process.env.IMHUB_NATIVE_AGENT_AUTOALLOW);
|
|
726
147
|
const denyList = splitEnvList(process.env.IMHUB_NATIVE_AGENT_DENYLIST);
|
|
727
|
-
// Allow the 3 safe native builtins + all first-party imhub MCP tools by
|
|
728
|
-
// default. The imhub tools are in-process (no external API cost, no
|
|
729
|
-
// network side-effects beyond what the IM bridge would do anyway) and
|
|
730
|
-
// are part of agim's own surface — denying them by default is the same
|
|
731
|
-
// class of misconfiguration as denying agim itself. Operators can
|
|
732
|
-
// remove individual entries via IMHUB_NATIVE_AGENT_DENYLIST.
|
|
733
148
|
const defaultBuiltins = [
|
|
734
149
|
'native_echo', 'native_now', 'native_random_uuid',
|
|
735
|
-
// fs read-only tools (v1.2.58). write_file deliberately omitted —
|
|
736
|
-
// it mutates the workspace, even though sensitive-paths + workspace
|
|
737
|
-
// restriction prevent escape; an IM card per write keeps operator
|
|
738
|
-
// awareness intact. Operators who trust native can add it via
|
|
739
|
-
// IMHUB_NATIVE_AGENT_AUTOALLOW=native_write_file.
|
|
740
150
|
'native_read_file', 'native_list_dir', 'native_glob', 'native_grep',
|
|
741
|
-
// web tools (v1.2.61) — read-only, safe-by-default. SSRF rules
|
|
742
|
-
// still apply to private IPs unless IMHUB_NATIVE_WEB_ALLOW_PRIVATE=1.
|
|
743
151
|
'native_web_fetch', 'native_web_search',
|
|
744
152
|
'mcp__imhub__read_skill', 'mcp__imhub__list_skills',
|
|
745
153
|
'mcp__imhub__save_memo', 'mcp__imhub__search_memos',
|
|
746
154
|
'mcp__imhub__update_memo', 'mcp__imhub__delete_memo',
|
|
747
155
|
'mcp__imhub__push_message', 'mcp__imhub__ask_user',
|
|
748
|
-
'mcp__imhub__call_agent',
|
|
749
|
-
|
|
750
|
-
// to the per-thread goal row, which is also the only thing the
|
|
751
|
-
// /goal slash command lets the user mutate directly. No external
|
|
752
|
-
// side effects, so safe-by-default.
|
|
753
|
-
'mcp__imhub__long_task', 'mcp__imhub__complete_goal',
|
|
754
|
-
// v1.2.124 — TodoWrite-style plan tracker. In-process, in-memory,
|
|
755
|
-
// per-thread; zero side effects beyond the model's own state. Safe
|
|
756
|
-
// to default-allow; never asks the user.
|
|
757
|
-
'native_todo_write',
|
|
758
|
-
// v1.2.131 — Plan-mode exit handshake. The tool's OWN dispatcher
|
|
759
|
-
// raises the user-facing approval card (with the plan markdown
|
|
760
|
-
// payload); auto-allowing here just keeps the policy gate from
|
|
761
|
-
// double-prompting. Without this entry an operator using allow-list
|
|
762
|
-
// mode would see two cards back-to-back ("native_exit_plan_mode
|
|
763
|
-
// ok?" then "approve plan?").
|
|
764
|
-
'native_exit_plan_mode',
|
|
156
|
+
'mcp__imhub__call_agent', 'mcp__imhub__long_task', 'mcp__imhub__complete_goal',
|
|
157
|
+
'native_todo_write', 'native_exit_plan_mode',
|
|
765
158
|
];
|
|
766
159
|
const effectiveAllow = mode === 'allow-list'
|
|
767
160
|
? Array.from(new Set([...autoAllow, ...defaultBuiltins]))
|
|
768
161
|
: autoAllow;
|
|
769
|
-
// v1.2.119 — PlanMode hard-denies write + exec tools.
|
|
770
|
-
// v1.2.120 — PlanMode now ALSO honours per-thread overrides written
|
|
771
|
-
// by the /plan slash command. Pass `threadKey` to apply
|
|
772
|
-
// the override; omit it (CLI boot context, tests) for
|
|
773
|
-
// the env-only path.
|
|
774
|
-
//
|
|
775
|
-
// Mirrors opencode's plan-mode behaviour: agent must produce a
|
|
776
|
-
// read-only plan; trying to call native_write_file / native_exec is
|
|
777
|
-
// denied with a clear structured message instructing the model to
|
|
778
|
-
// summarise the plan instead.
|
|
779
|
-
//
|
|
780
|
-
// We extend the user-supplied denyList rather than replacing the
|
|
781
|
-
// policy mode — read-only tools (Read/Grep/Glob/web_fetch/web_search/
|
|
782
|
-
// MCP read tools) continue to pass through autoAllow as before.
|
|
783
162
|
const effectiveDeny = isPlanModeOn(threadKey)
|
|
784
163
|
? Array.from(new Set([...denyList, ...PLAN_MODE_DENY_TOOLS]))
|
|
785
164
|
: denyList;
|
|
786
165
|
return { mode, autoAllow: effectiveAllow, denyList: effectiveDeny };
|
|
787
166
|
}
|
|
788
|
-
/** v1.2.119 — tools blocked when PlanMode is on. The list is intentionally
|
|
789
|
-
* narrow: writes to the workspace + arbitrary shell exec. Read-only
|
|
790
|
-
* fs/grep/glob/web/MCP tools stay enabled so the agent can still
|
|
791
|
-
* research and draft a plan. */
|
|
792
|
-
const PLAN_MODE_DENY_TOOLS = [
|
|
793
|
-
'native_write_file',
|
|
794
|
-
'native_exec',
|
|
795
|
-
];
|
|
796
|
-
/** Plan mode resolution (v1.2.120):
|
|
797
|
-
* - `threadKey` given (and matches a /plan-toggled row) → use that row
|
|
798
|
-
* - `threadKey` given but no row → fall through to env
|
|
799
|
-
* - `threadKey` omitted → env-only (back-compat for CLI boot, tests)
|
|
800
|
-
*/
|
|
801
167
|
export function isPlanModeOn(threadKey) {
|
|
802
168
|
return effectivePlanModeOn(threadKey);
|
|
803
169
|
}
|
|
804
|
-
/**
|
|
805
|
-
* v1.2.60 — bridge native's policy gate to the approval-bus so tools
|
|
806
|
-
* that the gate would otherwise silently deny instead surface an IM
|
|
807
|
-
* approval card. Mirrors how claude-code's MCP sidecar uses
|
|
808
|
-
* registerSyntheticPending. Resolves to 'allow' / 'deny' from the
|
|
809
|
-
* user's button tap / text reply / auto-allow rule. Throws (caught by
|
|
810
|
-
* the gate) when the bus can't reach the user — caller falls back to
|
|
811
|
-
* silent-deny in that case.
|
|
812
|
-
*/
|
|
813
|
-
// Exported for reuse by the pi-native engine, which builds the same IM
|
|
814
|
-
// approval-card escalation around its own approval gate.
|
|
815
170
|
export function buildNativeAskUser(opts) {
|
|
816
171
|
const baseCtx = {
|
|
817
172
|
platform: opts.platform,
|
|
@@ -821,7 +176,7 @@ export function buildNativeAskUser(opts) {
|
|
|
821
176
|
callerAgent: 'native',
|
|
822
177
|
};
|
|
823
178
|
return async (call) => {
|
|
824
|
-
const reqId =
|
|
179
|
+
const reqId = randomUUID();
|
|
825
180
|
return await new Promise((resolve, reject) => {
|
|
826
181
|
void approvalBus.registerSyntheticPending({
|
|
827
182
|
runId: opts.runId,
|
|
@@ -834,42 +189,13 @@ export function buildNativeAskUser(opts) {
|
|
|
834
189
|
});
|
|
835
190
|
};
|
|
836
191
|
}
|
|
837
|
-
function splitEnvList(raw) {
|
|
838
|
-
if (!raw)
|
|
839
|
-
return [];
|
|
840
|
-
return raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
841
|
-
}
|
|
842
|
-
/** Are we configured AT ALL to make tool calls? Reflected in startup
|
|
843
|
-
* log + onboarding hints so the operator notices when they ship
|
|
844
|
-
* `agents:['native']` without configuring a backend. */
|
|
845
|
-
function isConfigured() {
|
|
846
|
-
return pickProvider() !== null;
|
|
847
|
-
}
|
|
848
|
-
/**
|
|
849
|
-
* Extract media attachments from a prompt string. Messenger adapters
|
|
850
|
-
* (telegram / wechat / discord / feishu / dingtalk) inline a marker like
|
|
851
|
-
* `[图片附件:/abs/path/to/file.jpg]` or `[image attachment: ...]` when a
|
|
852
|
-
* user message carries a media payload. The path is already downloaded
|
|
853
|
-
* to `~/.agim/media/<platform>/...` by the adapter, so we only need to
|
|
854
|
-
* surface it to vision-capable providers; non-vision providers see the
|
|
855
|
-
* original text and can still acknowledge the attachment.
|
|
856
|
-
*
|
|
857
|
-
* Heuristic-safe: matches absolute paths under common image extensions
|
|
858
|
-
* + a path-safety check that the file exists. Anything ambiguous (no
|
|
859
|
-
* extension, file missing, points outside ~/.agim/media) is silently
|
|
860
|
-
* skipped so we never leak random filesystem paths into a vision call.
|
|
861
|
-
*/
|
|
862
192
|
export function parsePromptMedia(prompt) {
|
|
863
193
|
if (!prompt)
|
|
864
194
|
return [];
|
|
865
|
-
// Match both Chinese 图片附件 marker and English "image attachment".
|
|
866
|
-
// Capture group is the path between `:`/`: ` and `]`.
|
|
867
195
|
const re = /\[(?:图片附件|image attachment)[::]\s*([^\]]+)\]/g;
|
|
868
196
|
const out = [];
|
|
869
197
|
const home = process.env.HOME || '/root';
|
|
870
198
|
const mediaRootRaw = process.env.IMHUB_MEDIA_ROOT || `${home}/.agim/media`;
|
|
871
|
-
// Normalise media root once so the prefix check works regardless of
|
|
872
|
-
// trailing slashes / dotted segments in the env override.
|
|
873
199
|
const mediaRoot = pathResolve(mediaRootRaw);
|
|
874
200
|
const mediaPrefix = mediaRoot.endsWith(pathSep) ? mediaRoot : mediaRoot + pathSep;
|
|
875
201
|
for (;;) {
|
|
@@ -877,13 +203,7 @@ export function parsePromptMedia(prompt) {
|
|
|
877
203
|
if (m === null)
|
|
878
204
|
break;
|
|
879
205
|
const rawPath = (m[1] || '').trim();
|
|
880
|
-
if (!rawPath)
|
|
881
|
-
continue;
|
|
882
|
-
// Path-safety: must be absolute AND resolve to a real file UNDER
|
|
883
|
-
// the media root. Use path.resolve + sep-aware prefix check so
|
|
884
|
-
// siblings like `/root/.agim/media-evil/x` and traversals like
|
|
885
|
-
// `/root/.agim/media/../etc/x` are rejected.
|
|
886
|
-
if (!rawPath.startsWith('/'))
|
|
206
|
+
if (!rawPath || !rawPath.startsWith('/'))
|
|
887
207
|
continue;
|
|
888
208
|
const normalised = pathResolve(rawPath);
|
|
889
209
|
if (normalised !== mediaRoot && !normalised.startsWith(mediaPrefix))
|
|
@@ -897,582 +217,12 @@ export function parsePromptMedia(prompt) {
|
|
|
897
217
|
catch {
|
|
898
218
|
continue;
|
|
899
219
|
}
|
|
900
|
-
|
|
901
|
-
const lower = normalised.toLowerCase();
|
|
902
|
-
if (!/\.(jpg|jpeg|png|webp|gif)$/.test(lower))
|
|
220
|
+
if (!/\.(jpg|jpeg|png|webp|gif)$/i.test(normalised))
|
|
903
221
|
continue;
|
|
904
222
|
out.push({ path: normalised });
|
|
905
223
|
}
|
|
906
224
|
return out;
|
|
907
225
|
}
|
|
908
|
-
/**
|
|
909
|
-
* Tool-call heartbeat helper. Solves the "agim 没反应" anxiety during
|
|
910
|
-
* long tool calls (research-shaped tools / call_agent A2A turns can
|
|
911
|
-
* run > 10s under the 30-minute hard timeout).
|
|
912
|
-
*
|
|
913
|
-
* Behaviour:
|
|
914
|
-
* - When a tool call's onToolStart fires, schedule a one-shot push
|
|
915
|
-
* to the IM thread after IMHUB_NATIVE_HEARTBEAT_MS (default 6000).
|
|
916
|
-
* - When the call's onToolEnd fires first, cancel the pending push.
|
|
917
|
-
* - shutdown() clears any still-pending timers (turn finished cleanly).
|
|
918
|
-
*
|
|
919
|
-
* Suppression: handlePushOp uses the standard notification-evaluator
|
|
920
|
-
* gate + the per-user rate limit (10/min default). The heartbeat is
|
|
921
|
-
* intentionally short ("🔧 调用工具 X 中…") so the gate's "low signal"
|
|
922
|
-
* rule rarely drops it; if operators want them muted entirely, set
|
|
923
|
-
* IMHUB_NATIVE_HEARTBEAT_MS=0.
|
|
924
|
-
*/
|
|
925
|
-
function buildHeartbeats(runCtx) {
|
|
926
|
-
// Tool-level pulse: tools that take longer than IMHUB_NATIVE_HEARTBEAT_MS
|
|
927
|
-
// (default 6000) push a one-shot "🔧 调用工具 X 中…" so the user knows
|
|
928
|
-
// why the bot has gone quiet. Cancelled on tool end. Set 0 to disable.
|
|
929
|
-
const rawDelay = parseInt(process.env.IMHUB_NATIVE_HEARTBEAT_MS || '6000', 10);
|
|
930
|
-
const toolDelayMs = Number.isFinite(rawDelay) && rawDelay > 0 ? rawDelay : 0;
|
|
931
|
-
// Turn-level pulse: every IMHUB_NATIVE_TURN_HEARTBEAT_MS (default 180_000
|
|
932
|
-
// = 3 min) since turn start, push a "⏳ 还在处理(已 Nm)..." so a multi-
|
|
933
|
-
// step research turn that runs many sub-agents reassures the user it's
|
|
934
|
-
// still alive even between tool calls. Set 0 to disable. The first
|
|
935
|
-
// pulse fires after delayMs, NOT immediately, so short turns stay silent.
|
|
936
|
-
const rawTurnMs = parseInt(process.env.IMHUB_NATIVE_TURN_HEARTBEAT_MS || '180000', 10);
|
|
937
|
-
const turnDelayMs = Number.isFinite(rawTurnMs) && rawTurnMs > 0 ? rawTurnMs : 0;
|
|
938
|
-
const pending = new Map();
|
|
939
|
-
const startedAt = Date.now();
|
|
940
|
-
let turnTimer = null;
|
|
941
|
-
if (turnDelayMs > 0) {
|
|
942
|
-
turnTimer = setInterval(() => {
|
|
943
|
-
const elapsedMin = Math.round((Date.now() - startedAt) / 60_000);
|
|
944
|
-
void handlePushOp({ text: `⏳ 还在处理(已 ${elapsedMin}m)…` }, runCtx).catch(() => { });
|
|
945
|
-
}, turnDelayMs);
|
|
946
|
-
}
|
|
947
|
-
const shutdown = () => {
|
|
948
|
-
for (const t of pending.values())
|
|
949
|
-
clearTimeout(t);
|
|
950
|
-
pending.clear();
|
|
951
|
-
if (turnTimer) {
|
|
952
|
-
clearInterval(turnTimer);
|
|
953
|
-
turnTimer = null;
|
|
954
|
-
}
|
|
955
|
-
};
|
|
956
|
-
const noop = () => { };
|
|
957
|
-
if (toolDelayMs === 0)
|
|
958
|
-
return { hooks: { onToolStart: noop, onToolEnd: noop }, shutdown };
|
|
959
|
-
return {
|
|
960
|
-
hooks: {
|
|
961
|
-
onToolStart(call) {
|
|
962
|
-
const timer = setTimeout(() => {
|
|
963
|
-
void handlePushOp({ text: `🔧 调用工具 \`${call.name}\` 中…(已 ${Math.round(toolDelayMs / 1000)}s)` }, runCtx).catch(() => { });
|
|
964
|
-
pending.delete(call.id);
|
|
965
|
-
}, toolDelayMs);
|
|
966
|
-
pending.set(call.id, timer);
|
|
967
|
-
},
|
|
968
|
-
onToolEnd(call) {
|
|
969
|
-
const t = pending.get(call.id);
|
|
970
|
-
if (t) {
|
|
971
|
-
clearTimeout(t);
|
|
972
|
-
pending.delete(call.id);
|
|
973
|
-
}
|
|
974
|
-
},
|
|
975
|
-
},
|
|
976
|
-
shutdown,
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
// ─── AgentAdapter implementation ─────────────────────────────────────
|
|
980
|
-
class NativeAgentAdapter {
|
|
981
|
-
name = 'native';
|
|
982
|
-
aliases = ['agim', 'llm', 'native-llm', 'na'];
|
|
983
|
-
kind = 'in-process';
|
|
984
|
-
/** One-line UI hint surfaced by `/agents`: which LLM role + backend
|
|
985
|
-
* currently powers this adapter. Returns undefined when not
|
|
986
|
-
* configured (caller renders 'NOT CONFIGURED' elsewhere). */
|
|
987
|
-
describe() {
|
|
988
|
-
const picked = pickProvider();
|
|
989
|
-
if (!picked)
|
|
990
|
-
return undefined;
|
|
991
|
-
// 'role -> vendor:backend' so a glance at /agents tells you which
|
|
992
|
-
// LLM is wired without opening config.json.
|
|
993
|
-
return `role=${picked.role} -> ${picked.provider.providerType}:${picked.provider.name}`;
|
|
994
|
-
}
|
|
995
|
-
async isAvailable() {
|
|
996
|
-
return isConfigured();
|
|
997
|
-
}
|
|
998
|
-
async *sendPrompt(sessionId, prompt, history = [], opts = {}) {
|
|
999
|
-
const picked = pickProvider();
|
|
1000
|
-
if (!picked) {
|
|
1001
|
-
const msg = '❌ Agim Agent: no usable LLM backend configured. Add one in /settings/llm (or ~/.agim/config.json llmBackends + API key). Role bindings are optional.';
|
|
1002
|
-
yield msg;
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
const { provider, role } = picked;
|
|
1006
|
-
// Build the message array. The native loop accepts a system message
|
|
1007
|
-
// either as the first element OR via systemPrompt; we go with the
|
|
1008
|
-
// dedicated field so user-supplied history doesn't get clipped by
|
|
1009
|
-
// a synthetic system message.
|
|
1010
|
-
const messages = [];
|
|
1011
|
-
for (const m of history) {
|
|
1012
|
-
// ChatMessage role is 'user' | 'assistant'; both map straight
|
|
1013
|
-
// through. Skip empty content to keep prompts compact.
|
|
1014
|
-
if (!m.content)
|
|
1015
|
-
continue;
|
|
1016
|
-
messages.push({ role: m.role, content: m.content });
|
|
1017
|
-
}
|
|
1018
|
-
// Parse `[图片附件:/path/to/file]` / `[image attachment: ...]` markers
|
|
1019
|
-
// that messenger adapters inline into the prompt. Vision-capable
|
|
1020
|
-
// providers will encode them as image_url blocks; others just see
|
|
1021
|
-
// the original text and the model can acknowledge "an image was
|
|
1022
|
-
// attached at <path>" without inspecting bytes.
|
|
1023
|
-
const userMedia = parsePromptMedia(prompt);
|
|
1024
|
-
messages.push({ role: 'user', content: prompt, ...(userMedia.length > 0 ? { media: userMedia } : {}) });
|
|
1025
|
-
// Auto-compact long chats before the provider call. Op-out via
|
|
1026
|
-
// IMHUB_NATIVE_COMPACT_TRIGGER_CHARS=0. Failure mode is no-op.
|
|
1027
|
-
const compact = await maybeCompactHistory(messages);
|
|
1028
|
-
const effectiveMessages = compact.messages;
|
|
1029
|
-
if (compact.compacted) {
|
|
1030
|
-
log.info({
|
|
1031
|
-
event: 'native.compact.applied',
|
|
1032
|
-
sessionId,
|
|
1033
|
-
originalChars: compact.originalChars,
|
|
1034
|
-
summaryChars: compact.summaryChars,
|
|
1035
|
-
collapsedCount: compact.collapsedCount,
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
// Compose dispatcher: built-in first (so a stray MCP server with
|
|
1039
|
-
// a colliding tool name doesn't shadow `native_now` etc.), then
|
|
1040
|
-
// imhub built-ins (skills / memo / push / ask_user / call_agent),
|
|
1041
|
-
// then external MCP. v1.2.47 added the imhub layer so native sees
|
|
1042
|
-
// exactly the same mcp__imhub__* surface claude-code does via the
|
|
1043
|
-
// MCP sidecar.
|
|
1044
|
-
// v1.2.120 — compute the per-thread composite key once and thread
|
|
1045
|
-
// it through PlanMode + todo-state resolution. When the call has
|
|
1046
|
-
// no IM context (CLI / smoke), threadKey stays undefined → env-only
|
|
1047
|
-
// / synthetic-key fallback behaviour.
|
|
1048
|
-
const planThreadKey = (opts.platform && opts.threadId)
|
|
1049
|
-
? makeThreadKey(opts.platform, opts.channelId ?? '', opts.threadId)
|
|
1050
|
-
: undefined;
|
|
1051
|
-
const imhubCtx = {
|
|
1052
|
-
platform: opts.platform || 'native-agent',
|
|
1053
|
-
channelId: opts.channelId || 'default',
|
|
1054
|
-
threadId: opts.threadId || sessionId,
|
|
1055
|
-
userId: opts.userId || 'unknown',
|
|
1056
|
-
callerAgent: 'native',
|
|
1057
|
-
callerDepth: opts.callDepth ?? 0,
|
|
1058
|
-
// Link A2A callee rows to the parent turn so the web A2A views (which
|
|
1059
|
-
// filter parent_id IS NOT NULL) can see native-originated A2A.
|
|
1060
|
-
...(typeof opts.parentJobId === 'number' ? { parentJobId: opts.parentJobId } : {}),
|
|
1061
|
-
// v1.2.139 — propagate parent's plan-mode to the imhub dispatcher
|
|
1062
|
-
// so any call_agent invocation it makes hands the flag to the
|
|
1063
|
-
// sub-agent (claude-code / opencode / codex / native). Without
|
|
1064
|
-
// this, a parent in `/plan on` could delegate a write task and
|
|
1065
|
-
// the child would happily run with full write access. Falls back
|
|
1066
|
-
// to opts.planMode for A2A-initiated runs (sub-agent itself is
|
|
1067
|
-
// already in plan mode).
|
|
1068
|
-
callerPlanMode: effectivePlanModeOn(planThreadKey) || (opts.planMode === true),
|
|
1069
|
-
};
|
|
1070
|
-
// Resolve cwd here so fs-dispatcher can constrain reads/writes to
|
|
1071
|
-
// the per-thread workspace subtree. Was previously resolved AFTER
|
|
1072
|
-
// dispatch composition; moved up so fs tools see the right root.
|
|
1073
|
-
const cwd = resolveAgentCwd('native', opts) || defaultAgentCwd('native');
|
|
1074
|
-
// T2 (single tool registry): assemble the advertised tool list, the
|
|
1075
|
-
// dispatch chain, AND the per-call concurrency classifier from ONE
|
|
1076
|
-
// source-of-truth (see tool-registry.ts). This replaced three
|
|
1077
|
-
// hand-maintained parallel lists (tools[] / combineDispatchers / the
|
|
1078
|
-
// static parallelSafeTools Set) that silently drifted when a tool was
|
|
1079
|
-
// added. The plan-exit dispatcher is always wired (it self-refuses off
|
|
1080
|
-
// plan mode); its tool is advertised only when plan mode is on.
|
|
1081
|
-
const assembled = assembleNativeTools({
|
|
1082
|
-
cwd,
|
|
1083
|
-
rpcCtx: opts.platform && opts.threadId
|
|
1084
|
-
? {
|
|
1085
|
-
platform: opts.platform,
|
|
1086
|
-
channelId: opts.channelId ?? '',
|
|
1087
|
-
threadId: opts.threadId,
|
|
1088
|
-
userId: opts.userId ?? '',
|
|
1089
|
-
}
|
|
1090
|
-
: undefined,
|
|
1091
|
-
todoThreadKey: planThreadKey ?? `native:${sessionId}`,
|
|
1092
|
-
planExitCtx: {
|
|
1093
|
-
threadKey: planThreadKey ?? `native:${sessionId}`,
|
|
1094
|
-
runId: sessionId,
|
|
1095
|
-
platform: opts.platform,
|
|
1096
|
-
channelId: opts.channelId,
|
|
1097
|
-
threadId: opts.threadId,
|
|
1098
|
-
userId: opts.userId,
|
|
1099
|
-
},
|
|
1100
|
-
advertisePlanExit: !!(planThreadKey && effectivePlanModeOn(planThreadKey)),
|
|
1101
|
-
imhubCtx,
|
|
1102
|
-
});
|
|
1103
|
-
const tools = assembled.tools;
|
|
1104
|
-
const dispatch = assembled.dispatch;
|
|
1105
|
-
const policy = resolvePolicy(planThreadKey);
|
|
1106
|
-
// v1.2.60 — when the policy would silently deny a tool call,
|
|
1107
|
-
// escalate to the user via an IM approval card instead. Only
|
|
1108
|
-
// wires when we have an actual IM thread (platform + threadId);
|
|
1109
|
-
// CI / smoke-test runs without IM stay silent-deny so they don't
|
|
1110
|
-
// hang awaiting a notifier that doesn't exist.
|
|
1111
|
-
const askUser = (opts.platform && opts.threadId && approvalBus.hasNotifier())
|
|
1112
|
-
? buildNativeAskUser({
|
|
1113
|
-
runId: sessionId,
|
|
1114
|
-
platform: opts.platform,
|
|
1115
|
-
channelId: opts.channelId ?? 'default',
|
|
1116
|
-
threadId: opts.threadId,
|
|
1117
|
-
userId: opts.userId ?? 'unknown',
|
|
1118
|
-
})
|
|
1119
|
-
: undefined;
|
|
1120
|
-
const approve = buildPolicyApprovalGate({ ...policy, askUser });
|
|
1121
|
-
const startedAt = Date.now();
|
|
1122
|
-
log.info({
|
|
1123
|
-
event: 'native.turn.start',
|
|
1124
|
-
sessionId,
|
|
1125
|
-
role,
|
|
1126
|
-
backend: provider.name,
|
|
1127
|
-
policy: describePolicy(policy),
|
|
1128
|
-
tools: tools.length,
|
|
1129
|
-
platform: opts.platform,
|
|
1130
|
-
threadId: opts.threadId,
|
|
1131
|
-
});
|
|
1132
|
-
// Note: cwd is resolved above (before dispatch composition) so fs
|
|
1133
|
-
// tools can constrain reads/writes to the per-thread workspace.
|
|
1134
|
-
// Was previously resolved here — that was fine before native had
|
|
1135
|
-
// fs tools, but fs-dispatcher now needs the value at dispatch
|
|
1136
|
-
// build time.
|
|
1137
|
-
// ADR-0002 — prefer the inbound turn's trace id (plumbed via opts.traceId
|
|
1138
|
-
// from the router) so the native iteration / turn audit rows correlate
|
|
1139
|
-
// back to the originating IM message in SIEM joins. Fall back to a
|
|
1140
|
-
// self-minted id only for entry points that don't carry one (e.g. an A2A
|
|
1141
|
-
// in-process spawn or a direct CLI/smoke invocation).
|
|
1142
|
-
const traceId = opts.traceId || `native-${sessionId}-${Date.now()}`;
|
|
1143
|
-
// Wall-clock cap for the agent loop. agim's IM-layer enforces a 30-
|
|
1144
|
-
// minute hard ceiling per turn (DEFAULT_TIMEOUT_MS in agent-base.ts);
|
|
1145
|
-
// we set the inner loop a hair below so the loop's own abort fires
|
|
1146
|
-
// with a clean `finishReason='aborted'` BEFORE the outer SIGTERM. The
|
|
1147
|
-
// agent-loop default is a conservative 5 minutes which is far too
|
|
1148
|
-
// short for native turns that orchestrate sub-agents via call_agent
|
|
1149
|
-
// (each hop can run 1-3 minutes); without this override a multi-step
|
|
1150
|
-
// research turn would abort mid-flight even with sub-tasks healthy.
|
|
1151
|
-
// Operator can override via IMHUB_NATIVE_AGENT_TIMEOUT_MS.
|
|
1152
|
-
const nativeTimeoutMs = (() => {
|
|
1153
|
-
const raw = parseInt(process.env.IMHUB_NATIVE_AGENT_TIMEOUT_MS || '', 10);
|
|
1154
|
-
if (Number.isFinite(raw) && raw > 0)
|
|
1155
|
-
return raw;
|
|
1156
|
-
return 28 * 60 * 1000; // 28 min — leaves 2 min of IM-layer headroom
|
|
1157
|
-
})();
|
|
1158
|
-
const heartbeats = buildHeartbeats(imhubCtx);
|
|
1159
|
-
const chain = pickProviderChain();
|
|
1160
|
-
// chain always starts with `picked` from above — index 0 is the
|
|
1161
|
-
// primary; rest are fallbacks. We walk the chain only when the
|
|
1162
|
-
// PREVIOUS attempt ended with a transient provider error and the
|
|
1163
|
-
// turn produced no assistant text yet (so retrying is safe — no
|
|
1164
|
-
// duplicate replies).
|
|
1165
|
-
let result = null;
|
|
1166
|
-
let usedRole = role;
|
|
1167
|
-
let usedProvider = provider;
|
|
1168
|
-
for (let i = 0; i < chain.length; i++) {
|
|
1169
|
-
const candidate = chain[i];
|
|
1170
|
-
usedProvider = candidate.provider;
|
|
1171
|
-
usedRole = candidate.role;
|
|
1172
|
-
result = await runAgentLoop({
|
|
1173
|
-
provider: candidate.provider,
|
|
1174
|
-
systemPrompt: buildSystemPrompt(candidate.provider, candidate.role, cwd, planThreadKey),
|
|
1175
|
-
messages: effectiveMessages,
|
|
1176
|
-
tools,
|
|
1177
|
-
dispatch,
|
|
1178
|
-
approve,
|
|
1179
|
-
callOptions: { model: opts.model },
|
|
1180
|
-
// v1.2.92 — actually honour IMHUB_NATIVE_AGENT_MAX_ITER (the
|
|
1181
|
-
// banner has been advising operators to set it since v1.2.48
|
|
1182
|
-
// but the reader didn't exist; default stayed at 20).
|
|
1183
|
-
maxIterations: resolveMaxIterations(),
|
|
1184
|
-
// v1.2.98 — goal-critic anchor. agent-loop runs the critic
|
|
1185
|
-
// periodically; if it judges the recent tool chain off-track
|
|
1186
|
-
// we get finishReason='off_track' and render a redirect recap.
|
|
1187
|
-
// Pulls the active long-task goal lazily; failures are silent
|
|
1188
|
-
// (critic just won't have the goal anchor, will fall back to
|
|
1189
|
-
// the prompt). The critic itself is disabled when
|
|
1190
|
-
// IMHUB_NATIVE_CRITIC=off or no `cheap` role is configured.
|
|
1191
|
-
criticAnchor: await (async () => {
|
|
1192
|
-
let goalTitle;
|
|
1193
|
-
let goalBody;
|
|
1194
|
-
if (opts.platform && opts.threadId) {
|
|
1195
|
-
try {
|
|
1196
|
-
const { getActiveGoal } = await import('../../../core/goals.js');
|
|
1197
|
-
const g = getActiveGoal(opts.platform, opts.channelId ?? '', opts.threadId);
|
|
1198
|
-
if (g) {
|
|
1199
|
-
goalTitle = g.title;
|
|
1200
|
-
goalBody = g.body ?? undefined;
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
catch { /* best-effort */ }
|
|
1204
|
-
}
|
|
1205
|
-
return { prompt, goalTitle, goalBody };
|
|
1206
|
-
})(),
|
|
1207
|
-
timeoutMs: nativeTimeoutMs,
|
|
1208
|
-
signal: opts.signal,
|
|
1209
|
-
audit: {
|
|
1210
|
-
agent: `llm:${candidate.provider.name}`,
|
|
1211
|
-
intent: 'native.agent.iter',
|
|
1212
|
-
userId: opts.userId,
|
|
1213
|
-
platform: opts.platform || 'native-agent',
|
|
1214
|
-
traceId,
|
|
1215
|
-
},
|
|
1216
|
-
hooks: heartbeats.hooks,
|
|
1217
|
-
// v1.2.109 / T2 — declare read-only / pure tools parallel-safe so
|
|
1218
|
-
// multi-call iterations (e.g. "read these 3 files") run
|
|
1219
|
-
// concurrently. Now a PER-CALL classifier owned by the tool
|
|
1220
|
-
// registry (fail-closed: unknown / throwing → serial), replacing
|
|
1221
|
-
// the static per-name set.
|
|
1222
|
-
parallelSafeClassifier: assembled.isParallelSafe,
|
|
1223
|
-
// v1.2.112 — stream provider responses so partial assistant
|
|
1224
|
-
// text survives the IM 30-min hard timeout. Env-gated kill
|
|
1225
|
-
// switch (`IMHUB_NATIVE_STREAM_PARTIAL=off` + global
|
|
1226
|
-
// `IMHUB_AGENT_LOOP_STREAM=off`) for safety. No onPartialText
|
|
1227
|
-
// wired yet — that lands when we push streaming to the IM
|
|
1228
|
-
// client. The accumulation itself already saves the partial.
|
|
1229
|
-
streamPartialText: resolveNativeStreamPartial(),
|
|
1230
|
-
});
|
|
1231
|
-
if (result.finishReason !== 'error')
|
|
1232
|
-
break;
|
|
1233
|
-
const errStr = String(result.error || '');
|
|
1234
|
-
const isTransient = /5\d\d|timeout|ECONN|ETIMEDOUT|fetch failed|socket hang up|408|network/i.test(errStr);
|
|
1235
|
-
const hasText = result.text && result.text.length > 0;
|
|
1236
|
-
if (!isTransient || hasText || i === chain.length - 1)
|
|
1237
|
-
break;
|
|
1238
|
-
log.warn({
|
|
1239
|
-
event: 'native.fallback.next',
|
|
1240
|
-
from: candidate.provider.name,
|
|
1241
|
-
nextIdx: i + 1,
|
|
1242
|
-
err: errStr,
|
|
1243
|
-
}, `provider ${candidate.provider.name} transient-failed; trying next fallback`);
|
|
1244
|
-
}
|
|
1245
|
-
heartbeats.shutdown();
|
|
1246
|
-
if (!result) {
|
|
1247
|
-
// Shouldn't happen — pickProvider above already returned the primary
|
|
1248
|
-
// so the chain has at least one entry. Defensive belt-and-suspenders.
|
|
1249
|
-
yield '❌ Agim Agent: provider chain ended without any attempt';
|
|
1250
|
-
return;
|
|
1251
|
-
}
|
|
1252
|
-
// v1.2.142 — Stage report. Replaces v1.2.94 auto-summary.
|
|
1253
|
-
//
|
|
1254
|
-
// Any unhappy turn ending (empty / max_iter / stuck_loop / off_track)
|
|
1255
|
-
// first tries to produce a *user-facing stage report* with the
|
|
1256
|
-
// current provider — based on what got done, what failed, and what
|
|
1257
|
-
// to do next. The technical "✓ tool ×N / ✗ tool ×M" recap from
|
|
1258
|
-
// v1.2.91 is kept ONLY as a last-resort fallback when the stage
|
|
1259
|
-
// report itself fails or comes back empty.
|
|
1260
|
-
//
|
|
1261
|
-
// Why the change: the operator-facing message previously dumped
|
|
1262
|
-
// tool-name counts, which is debugger fodder, not a deliverable.
|
|
1263
|
-
// Even a half-failed turn has a real *intermediate result* the
|
|
1264
|
-
// user can act on — the model just needs to be asked the right
|
|
1265
|
-
// question. See `tryStageReport` for the prompt.
|
|
1266
|
-
//
|
|
1267
|
-
// The empty/stop branch's logic now lives inside `tryStageReport`
|
|
1268
|
-
// — empty `result.text` is one of four kinds it covers, not a
|
|
1269
|
-
// special case.
|
|
1270
|
-
// Per-turn parent audit row that aggregates the iteration rows
|
|
1271
|
-
// already written by runAgentLoop. Lets /tasks#cost sum cost per
|
|
1272
|
-
// turn rather than per iteration.
|
|
1273
|
-
//
|
|
1274
|
-
// NOTE — moved BELOW the body-assembly block in v1.2.142 so the
|
|
1275
|
-
// stage-report retry cost is counted in this row. (Was above when
|
|
1276
|
-
// auto-summary mutated `result.usage` in place; the new flow
|
|
1277
|
-
// returns a separate `extraCost` from tryStageReport instead, so
|
|
1278
|
-
// we wait until after body assembly to log.)
|
|
1279
|
-
// ─── body assembly ───────────────────────────────────────────────
|
|
1280
|
-
//
|
|
1281
|
-
// Compose the user-facing reply. v1.2.142 reshuffles the order:
|
|
1282
|
-
// each unhappy branch FIRST tries `tryStageReport` (a natural-
|
|
1283
|
-
// language stage report based on what got done + what failed +
|
|
1284
|
-
// what to do next). Technical `composeUnfinishedTurnRecap` is
|
|
1285
|
-
// demoted to last-resort fallback when the stage report itself
|
|
1286
|
-
// fails / returns empty.
|
|
1287
|
-
let body = result.text;
|
|
1288
|
-
/** Extra cost from the stage-report retry (when it runs). Added to
|
|
1289
|
-
* the audit row + opts.onUsage below so /tasks#cost stays
|
|
1290
|
-
* accurate. */
|
|
1291
|
-
let stageReportCost = 0;
|
|
1292
|
-
const maxIter = resolveMaxIterations();
|
|
1293
|
-
/** Local helper — log the unhappy branch + try stage report.
|
|
1294
|
-
* Returns the stage-report text on success; null when caller
|
|
1295
|
-
* should fall back to its technical recap. */
|
|
1296
|
-
const stageOrFallback = async (kind, offTrackReason) => {
|
|
1297
|
-
const stage = await tryStageReport({
|
|
1298
|
-
prompt,
|
|
1299
|
-
result,
|
|
1300
|
-
provider: usedProvider,
|
|
1301
|
-
kind,
|
|
1302
|
-
offTrackReason,
|
|
1303
|
-
model: opts.model,
|
|
1304
|
-
signal: opts.signal,
|
|
1305
|
-
sessionId,
|
|
1306
|
-
});
|
|
1307
|
-
if (stage) {
|
|
1308
|
-
stageReportCost += stage.costUsd ?? 0;
|
|
1309
|
-
return stage.text;
|
|
1310
|
-
}
|
|
1311
|
-
return null;
|
|
1312
|
-
};
|
|
1313
|
-
if (result.finishReason === 'error') {
|
|
1314
|
-
body = `❌ Agim Agent error: ${result.error ?? '(no detail)'}`;
|
|
1315
|
-
}
|
|
1316
|
-
else if (result.finishReason === 'max_iterations') {
|
|
1317
|
-
log.warn({
|
|
1318
|
-
event: 'native.turn.max_iterations',
|
|
1319
|
-
sessionId,
|
|
1320
|
-
backend: usedProvider.name,
|
|
1321
|
-
role: usedRole,
|
|
1322
|
-
iterations: result.iterations,
|
|
1323
|
-
maxIter,
|
|
1324
|
-
toolCallCount: result.toolCalls.length,
|
|
1325
|
-
lastToolName: result.toolCalls.length > 0
|
|
1326
|
-
? result.toolCalls[result.toolCalls.length - 1]?.name ?? null
|
|
1327
|
-
: null,
|
|
1328
|
-
elapsedMs: Date.now() - startedAt,
|
|
1329
|
-
}, `native turn hit max iterations cap (${result.iterations}/${maxIter}, tools=${result.toolCalls.length})`);
|
|
1330
|
-
body = (await stageOrFallback('max_iter'))
|
|
1331
|
-
?? composeUnfinishedTurnRecap(result, 'max_iter', maxIter);
|
|
1332
|
-
}
|
|
1333
|
-
else if (result.finishReason === 'off_track') {
|
|
1334
|
-
// v1.2.98 — goal-critic flagged the recent tool chain as
|
|
1335
|
-
// semantically off-target. result.error carries
|
|
1336
|
-
// "<reason> || redirect: <suggestion>" (or just <reason> when
|
|
1337
|
-
// the critic had no redirect to offer). We split it back and
|
|
1338
|
-
// surface a recap that names the suspected drift + suggestion.
|
|
1339
|
-
const blob = String(result.error ?? '');
|
|
1340
|
-
const [reason, ...rest] = blob.split('|| redirect: ');
|
|
1341
|
-
const redirect = rest.join('|| redirect: ').trim();
|
|
1342
|
-
log.warn({
|
|
1343
|
-
event: 'native.turn.off_track',
|
|
1344
|
-
sessionId,
|
|
1345
|
-
backend: usedProvider.name,
|
|
1346
|
-
role: usedRole,
|
|
1347
|
-
iterations: result.iterations,
|
|
1348
|
-
toolCallCount: result.toolCalls.length,
|
|
1349
|
-
reason: reason.trim(),
|
|
1350
|
-
redirect: redirect || null,
|
|
1351
|
-
elapsedMs: Date.now() - startedAt,
|
|
1352
|
-
}, `goal-critic flagged turn as off-track: ${reason.trim()}`);
|
|
1353
|
-
const offTrackReason = reason.trim() + (redirect ? `;建议方向:${redirect}` : '');
|
|
1354
|
-
body = (await stageOrFallback('off_track', offTrackReason))
|
|
1355
|
-
?? composeOffTrackRecap(result, reason.trim(), redirect);
|
|
1356
|
-
}
|
|
1357
|
-
else if (result.finishReason === 'stuck_loop') {
|
|
1358
|
-
log.warn({
|
|
1359
|
-
event: 'native.turn.stuck_loop',
|
|
1360
|
-
sessionId,
|
|
1361
|
-
backend: usedProvider.name,
|
|
1362
|
-
role: usedRole,
|
|
1363
|
-
iterations: result.iterations,
|
|
1364
|
-
toolCallCount: result.toolCalls.length,
|
|
1365
|
-
lastToolName: result.toolCalls.length > 0
|
|
1366
|
-
? result.toolCalls[result.toolCalls.length - 1]?.name ?? null
|
|
1367
|
-
: null,
|
|
1368
|
-
elapsedMs: Date.now() - startedAt,
|
|
1369
|
-
}, `native turn stopped early — stuck loop after ${result.iterations} iter (tools=${result.toolCalls.length})`);
|
|
1370
|
-
body = (await stageOrFallback('stuck_loop'))
|
|
1371
|
-
?? composeUnfinishedTurnRecap(result, 'stuck_loop', maxIter);
|
|
1372
|
-
}
|
|
1373
|
-
else if (result.finishReason === 'aborted') {
|
|
1374
|
-
body = '⏹ Agim Agent aborted before completion.';
|
|
1375
|
-
}
|
|
1376
|
-
else if (result.finishReason === 'hallucinated_tools') {
|
|
1377
|
-
// v1.2.147 — agent-loop detected the model narrated a tool
|
|
1378
|
-
// invocation ("我现在调用 native_write_file: ```python …```")
|
|
1379
|
-
// without actually emitting toolCalls. Surface a recap that
|
|
1380
|
-
// names the failure mode + suggests a backend switch, instead
|
|
1381
|
-
// of shipping the lie as a normal reply.
|
|
1382
|
-
log.warn({
|
|
1383
|
-
event: 'native.turn.hallucinated_tools',
|
|
1384
|
-
sessionId,
|
|
1385
|
-
backend: usedProvider.name,
|
|
1386
|
-
role: usedRole,
|
|
1387
|
-
iterations: result.iterations,
|
|
1388
|
-
toolCallCount: result.toolCalls.length,
|
|
1389
|
-
textLen: (result.text || '').length,
|
|
1390
|
-
elapsedMs: Date.now() - startedAt,
|
|
1391
|
-
}, `native turn ended with hallucinated tool-call narration (no real toolCalls emitted)`);
|
|
1392
|
-
body = composeHallucinatedToolRecap(result, usedProvider.name);
|
|
1393
|
-
}
|
|
1394
|
-
else if (!body) {
|
|
1395
|
-
// Normal `stop` finish but the model didn't write anything. Most
|
|
1396
|
-
// often the model completed a tool chain and forgot to close
|
|
1397
|
-
// (or the chain failed badly enough that it gave up). Stage
|
|
1398
|
-
// report turns either case into a useful user-facing summary.
|
|
1399
|
-
const lastCall = result.toolCalls.length > 0
|
|
1400
|
-
? result.toolCalls[result.toolCalls.length - 1]
|
|
1401
|
-
: null;
|
|
1402
|
-
log.warn({
|
|
1403
|
-
event: 'native.turn.empty_response',
|
|
1404
|
-
sessionId,
|
|
1405
|
-
backend: usedProvider.name,
|
|
1406
|
-
role: usedRole,
|
|
1407
|
-
finishReason: result.finishReason,
|
|
1408
|
-
iterations: result.iterations,
|
|
1409
|
-
toolCallCount: result.toolCalls.length,
|
|
1410
|
-
lastToolName: lastCall?.name ?? null,
|
|
1411
|
-
lastToolError: lastCall?.isError ?? null,
|
|
1412
|
-
lastToolPreview: lastCall?.preview?.slice(0, 200) ?? null,
|
|
1413
|
-
elapsedMs: Date.now() - startedAt,
|
|
1414
|
-
}, `native turn ended with empty text (finishReason=${result.finishReason}, ` +
|
|
1415
|
-
`iterations=${result.iterations}, tools=${result.toolCalls.length})`);
|
|
1416
|
-
// Stage report only runs when there ARE tool calls to summarise;
|
|
1417
|
-
// an empty turn with zero tools means the model literally said
|
|
1418
|
-
// nothing — recap's "no tools called" branch handles that case.
|
|
1419
|
-
if (result.toolCalls.length > 0) {
|
|
1420
|
-
body = (await stageOrFallback('empty'))
|
|
1421
|
-
?? composeUnfinishedTurnRecap(result, 'empty', maxIter);
|
|
1422
|
-
}
|
|
1423
|
-
else {
|
|
1424
|
-
body = composeUnfinishedTurnRecap(result, 'empty', maxIter);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
// ─── audit + usage (v1.2.142 moved below body assembly) ──────────
|
|
1428
|
-
// Per-turn parent audit row that aggregates the iteration rows
|
|
1429
|
-
// already written by runAgentLoop. `stageReportCost` covers any
|
|
1430
|
-
// extra LLM call we made while composing the user-facing message.
|
|
1431
|
-
const turnCostUsd = (typeof result.usage.costUsd === 'number' ? result.usage.costUsd : 0)
|
|
1432
|
-
+ stageReportCost;
|
|
1433
|
-
try {
|
|
1434
|
-
logInvocation({
|
|
1435
|
-
traceId,
|
|
1436
|
-
userId: opts.userId ?? '',
|
|
1437
|
-
platform: opts.platform || 'native-agent',
|
|
1438
|
-
agent: this.name,
|
|
1439
|
-
intent: 'native.agent.turn',
|
|
1440
|
-
promptLen: prompt.length,
|
|
1441
|
-
responseLen: body.length,
|
|
1442
|
-
durationMs: Date.now() - startedAt,
|
|
1443
|
-
cost: turnCostUsd,
|
|
1444
|
-
success: result.finishReason !== 'error' && result.finishReason !== 'aborted',
|
|
1445
|
-
error: result.error,
|
|
1446
|
-
});
|
|
1447
|
-
}
|
|
1448
|
-
catch { /* audit best-effort */ }
|
|
1449
|
-
// Surface usage to cli's per-session accumulator the same way CLI
|
|
1450
|
-
// adapters do (via opts.onUsage).
|
|
1451
|
-
if (opts.onUsage && turnCostUsd > 0) {
|
|
1452
|
-
try {
|
|
1453
|
-
opts.onUsage({ costUsd: turnCostUsd });
|
|
1454
|
-
}
|
|
1455
|
-
catch { /* best-effort */ }
|
|
1456
|
-
}
|
|
1457
|
-
log.info({
|
|
1458
|
-
event: 'native.turn.done',
|
|
1459
|
-
sessionId,
|
|
1460
|
-
backend: usedProvider.name,
|
|
1461
|
-
role: usedRole,
|
|
1462
|
-
fellBack: usedProvider.name !== provider.name,
|
|
1463
|
-
finishReason: result.finishReason,
|
|
1464
|
-
iterations: result.iterations,
|
|
1465
|
-
toolCalls: result.toolCalls.length,
|
|
1466
|
-
stageReportCostUsd: stageReportCost > 0 ? stageReportCost : null,
|
|
1467
|
-
elapsedMs: Date.now() - startedAt,
|
|
1468
|
-
});
|
|
1469
|
-
yield body;
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
export const nativeAgentAdapter = new NativeAgentAdapter();
|
|
1473
|
-
/** Lightweight banner for cli.ts boot log. Lets operators see at a
|
|
1474
|
-
* glance whether `/cc native` will work before they try it in an IM
|
|
1475
|
-
* thread. */
|
|
1476
226
|
export function describeNativeAgent() {
|
|
1477
227
|
const picked = pickProvider();
|
|
1478
228
|
if (!picked)
|