chainlesschain 0.162.30 → 0.162.32
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/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-CsNttUU7.js → AIOps-Cg_uWAVl.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-lgohjckQ.js → ActionButton-DSFtQ1c2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-ccV3LAca.js → Analytics-BMxpkw8y.js} +3 -3
- package/src/assets/web-panel/assets/AppLayout-tgVxlmsx.js +9 -0
- package/src/assets/web-panel/assets/{Audit-B1gFM5U9.js → Audit-DwzGllcp.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BeWE3ERo.js → Backup-BG28Y2MV.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CDkPsNG2.js → BaseInput-TXthbazl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-ztb9ia6e.js → Chat-D096SxaD.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Dlw_6n3M.js → ChatBubbleRenderer-PIx0Eu9I.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BcfRBlIY.js → Checkbox-Czttw1JS.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DOs99xkr.js → Codegen-DZtMgv4q.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D1X6tYlj.js → Col-D3DnfExY.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DTksIWtz.js → Community-Bj5AdwqY.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DIJtAYBO.js → Compact-BQ8Zszub.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BBf7LF_k.js → Compliance-DXacb34n.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-UBPXQ40s.js → Cowork-BgMUBTkw.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CkRm1jPB.js → Cron-fqBWOqlN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-qALlTl7e.js → Crosschain-E4oa1MWy.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CqyqVS6E.js → DID-pwgfYZaV.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-n8mdLFIR.js +3 -0
- package/src/assets/web-panel/assets/{Dropdown-Cb5UzbSZ.js → Dropdown--6DYqxk7.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CarBq8Fk.js → EmailListRenderer-CkjQluz3.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CSiGXaZz.js → FamilyGuardDashboard-u-QTQ-OC.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DUxhVoBN.js → Federation-D219M5Qc.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BoMQpkhx.js → FormItemContext-BBU_aopC.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DTVqC_CX.js → GenericCardRenderer-pTMCIHcM.js} +1 -1
- package/src/assets/web-panel/assets/{Git-C_XuPtK5.js → Git-ClcCARWt.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BZyqlqz-.js → Governance-CvUi3I93.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DdZVUimI.js → Inference-DT-a4pVg.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-IzZ-jnCn.js → KnowledgeGraph-DHMs2LY8.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-koTK6eNc.js → Logs-D2s4eV1N.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-6zpJ1L8n.js → Marketplace-YC5-fx-6.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Ywc4IVks.js → McpTools-7JHTEC4T.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-C_zB9dUa.js → Memory-BudotVLD.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Nc05r24L.js → MobileBridge-CAiRyLVU.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-BJGxL526.js → MobileProjects-CrJJOCFw.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Im7SIcz1.js → Mtc-d0iY0CeK.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BFFzvzMD.js → MtcAudit-aI2cG1UP.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-CcNEbycq.js → Multisig-4bF70khG.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CDH6OTXN.js → NLProgramming-CwLib1S7.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Dqg3QXcU.js → Notes-Wt7AuFRU.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CDVmK1eU.js → NotificationSettings-D081vV_7.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-DCPei1L9.js +1 -0
- package/src/assets/web-panel/assets/{Organization-DJb9bRQS.js → Organization-BNEsUNdP.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CK7Q5dje.js → Overflow-B_1iUXDD.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CJIyYfwc.js → P2P-Dbc-kNwJ.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-uqRULcuw.js → PdhVaultBrowser-D8Xh289k.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-Crvwt6bq.js → Permissions-C77mM6-n.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DcN5OWzg.js → PersonalDataHub-Dj0J3r_K.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DfWJvvJW.js → Pipeline-B6F0WQ2C.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DepD0S3v.js → Privacy-eDKOkyyq.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B7OKhH27.js → ProjectInit-DAWwhr5_.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BJ4ueRFv.js → ProjectSettings-DwdK8k6I.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Cb3p5QAP.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Dl0FT1S3.js → Providers--DcYxQfN.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-V2hYLhfp.js → QuickAsk-DU268niT.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-8Kaiodgv.js → Recommend-ChnflhV1.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CsxB3JGg.js → Reputation-DSsY3bQG.js} +1 -1
- package/src/assets/web-panel/assets/{Row-6-x7tEYq.js → Row-Zb-EjmgQ.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Buv6f5tw.js → RssFeed-CGLiixZB.js} +3 -3
- package/src/assets/web-panel/assets/{Search-ABrDz84n.js → Search-Dhr_po-U.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DqOJmz18.js → Security-GMYNhGsR.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cq4Tda3q.js → Services-DiOpnVY0.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-n74QlyYq.js → Skeleton-DG3ez6ME.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CC0iozL5.js → Skills-DZGptytP.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-hwRgJ99Z.js → Sla-CtGpE3xA.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B6Bs6_-8.js → SpeechSettings-DQFw6Cf9.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CTp2dZ0z.js → SyncSettings-C8X78RpX.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-D70Lis6S.js → Tasks-DtVkhWCV.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Cags0ssw.js → Templates-SF9_ZWsV.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BxCMzzGt.js → Tenant-BbIQSVZz.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-v05SDqHd.js → Terminal-DKr5zDwu.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BLUDHbBL.js → TimelineRenderer-BtLaNaWr.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-D-xKLJYv.js → Tokens-CfYbk2NG.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B47tVIbH.js → Trigger-BLX_XDP0.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DmRU9kfs.js → Trust-BWxUv9PR.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DzgSGs-c.js → UkeySign-DRwTyQD4.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C6qu58up.js → VideoEditing-BsC4VOSo.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dh8ZWx8f.js → Wallet-CSsO1NJU.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DFHOVuAY.js → WebAuthn-z1MxiFzS.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B_fyQ3Y_.js → WorkflowEditor-B1vV7uuJ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BR-WxnCQ.js → chat-C0NJRaL2.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C-6RysQe.js → colors-CHRiteWF.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-B_9_SCKN.js → compact-item-2XmBBKPD.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D6rklIbE.js → createContext-DkedHC38.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DmNpkOdC.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BrotgSvd.js → hasIn-Bpn9Xrlw.js} +1 -1
- package/src/assets/web-panel/assets/index-7nAysteg.js +1 -0
- package/src/assets/web-panel/assets/{index-MCmNzIC7.js → index-B5NGWgHp.js} +1 -1
- package/src/assets/web-panel/assets/{index-GzuCTHVZ.js → index-BItcSqan.js} +3 -3
- package/src/assets/web-panel/assets/index-BKWSQilQ.js +1 -0
- package/src/assets/web-panel/assets/{index-DTCUOKu9.js → index-BN068mCR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bv9BrnD2.js → index-BOsIgPge.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfqUsPl2.js → index-BYUd69vM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn21XmDt.js → index-BYmwEaIk.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWmJukRW.js → index-BZ1gOoiG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bwkg_EJk.js → index-BfY9U3X5.js} +1 -1
- package/src/assets/web-panel/assets/{index-MBOwmoOi.js → index-BveL_4n3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJ70GAW2.js → index-CCg6ZY4t.js} +1 -1
- package/src/assets/web-panel/assets/{index-B85rQNYG.js → index-CJOoo72F.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn5ghmbB.js → index-CToQxpWz.js} +1 -1
- package/src/assets/web-panel/assets/{index-rWiOF7Iu.js → index-CWgWrrWs.js} +1 -1
- package/src/assets/web-panel/assets/{index-PzM_GlKb.js → index-CdR7RfRP.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZehgEQYa.js → index-Cljnfuxu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsDNNDBN.js → index-CxvA72CP.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6KqyxG1.js → index-CyJpmSHZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-E_5VXq8H.js → index-D7U411hK.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgp_QFo.js → index-D9mNfpxi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTpElYJs.js → index-DAFLFMXQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMnomft7.js → index-DAeHmElB.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2yXH6vy.js → index-DDy_RDjs.js} +1 -1
- package/src/assets/web-panel/assets/{index-kkjq_hwC.js → index-DE5Qm9UI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTh0fWI4.js → index-DM9JrnYi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DigjvHuo.js → index-DMbF-Euw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DkpDFJRn.js → index-DUBsq_1G.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIiCIC2j.js → index-De49R7TX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsWVDOd2.js → index-De5vOO9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAfRNHna.js → index-Dk7P-q3n.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdDmzoPE.js → index-DryKGM_t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTQkYbir.js → index-DtU4qZRF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CK8YwdNd.js → index-NuBsCRaR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaLhL3Tj.js → index-Sk3-3tKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTpxOc5s.js → index-alGjpoM1.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrGp-4E2.js → index-cfSUlOfY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbRl_gIW.js → index-i4W_EAuh.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCWzUY8K.js → index-uHGxyZtQ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C2v_L5na.js → initDefaultProps-DlDE-QgI.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DNDqGbfr.js → motion-CodUbIRF.js} +1 -1
- package/src/assets/web-panel/assets/{move-xvpQ_6hJ.js → move-DaLwsHeR.js} +1 -1
- package/src/assets/web-panel/assets/{omit-Cb0FsfrO.js → omit-DdVg-3rL.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BxhYpnum.js → pickAttrs-KLR1EVCo.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B3soaW4h.js → placementArrow-ChV7HvNw.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B-eRSLvd.js → responsiveObserve-BB_A8dBt.js} +1 -1
- package/src/assets/web-panel/assets/{slide--cM2ZOx-.js → slide-Bc1tQnIK.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DjBhfi8Q.js → statusUtils-CgrveSb0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-C30mMh8o.js → styleChecker-vXAYhhjz.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-f7y2Qlzs.js → useFlexGapSupport-BCIMPfq9.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-iTCXoLoZ.js → useFs-DMZGdr6G.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BH0RXmVF.js → usePersonalDataHub-118tWI_Z.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DQtmeDXM.js → vnode-Z7O2Y7JP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-vw50zkLZ.js → zoom-BXym6zmD.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +333 -1
- package/src/commands/ask.js +35 -1
- package/src/commands/checkpoint.js +439 -0
- package/src/commands/compact.js +150 -0
- package/src/commands/cost.js +114 -0
- package/src/commands/goal.js +417 -0
- package/src/commands/hub.js +7 -0
- package/src/commands/session.js +22 -2
- package/src/harness/prompt-compressor.js +71 -1
- package/src/index.js +8 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/checkpoint-store.js +523 -0
- package/src/lib/file-checkpoint.js +300 -0
- package/src/lib/goal-context.js +87 -0
- package/src/lib/goal-store.js +308 -0
- package/src/lib/llm-pricing.js +227 -0
- package/src/lib/personal-data-hub-wiring.js +30 -0
- package/src/lib/recent-session.js +72 -0
- package/src/lib/session-picker.js +68 -0
- package/src/repl/agent-repl.js +101 -9
- package/src/repl/chat-repl.js +16 -1
- package/src/runtime/agent-core.js +313 -32
- package/src/runtime/fallback-model.js +109 -0
- package/src/runtime/file-ref-expander.js +258 -0
- package/src/runtime/headless-runner.js +601 -0
- package/src/runtime/headless-stream.js +315 -0
- package/src/runtime/policies/agent-policy.js +7 -0
- package/src/runtime/quiet-stdout.js +35 -0
- package/src/runtime/system-prompt.js +60 -0
- package/src/assets/web-panel/assets/AppLayout-B0hl5cPk.js +0 -9
- package/src/assets/web-panel/assets/Dashboard-XlMpT7K_.js +0 -3
- package/src/assets/web-panel/assets/OrderTableRenderer-Bg0bkfjR.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dl_hPdhU.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BiN5HELJ.js +0 -1
- package/src/assets/web-panel/assets/index-BhxiT2LJ.js +0 -1
- package/src/assets/web-panel/assets/index-DBNSZ2oz.js +0 -1
|
@@ -338,9 +338,12 @@ function _buildPersonaPrompt(persona, envLines, cwd) {
|
|
|
338
338
|
* 4. Default hardcoded prompt → fallback when no persona
|
|
339
339
|
*
|
|
340
340
|
* @param {string} [cwd] - working directory
|
|
341
|
+
* @param {object} [opts]
|
|
342
|
+
* @param {string[]} [opts.additionalDirectories] - extra workspace roots
|
|
343
|
+
* (absolute paths) the agent may read/search/edit beyond `cwd`.
|
|
341
344
|
* @returns {string} complete system prompt
|
|
342
345
|
*/
|
|
343
|
-
export function buildSystemPrompt(cwd) {
|
|
346
|
+
export function buildSystemPrompt(cwd, opts = {}) {
|
|
344
347
|
const dir = cwd || process.cwd();
|
|
345
348
|
|
|
346
349
|
// Check for project persona
|
|
@@ -393,6 +396,19 @@ export function buildSystemPrompt(cwd) {
|
|
|
393
396
|
// Non-critical
|
|
394
397
|
}
|
|
395
398
|
|
|
399
|
+
// Advertise extra workspace roots (--add-dir) so the model knows it may
|
|
400
|
+
// reach beyond cwd and which absolute paths to use.
|
|
401
|
+
const extraDirs = Array.isArray(opts.additionalDirectories)
|
|
402
|
+
? opts.additionalDirectories.filter(Boolean)
|
|
403
|
+
: [];
|
|
404
|
+
if (extraDirs.length > 0) {
|
|
405
|
+
prompt +=
|
|
406
|
+
`\n\n## Additional working directories\n` +
|
|
407
|
+
`Beyond the current working directory, you may read, search, and edit ` +
|
|
408
|
+
`files under these absolute roots. Pass absolute paths to access them:\n` +
|
|
409
|
+
extraDirs.map((d) => `- ${d}`).join("\n");
|
|
410
|
+
}
|
|
411
|
+
|
|
396
412
|
return prompt;
|
|
397
413
|
}
|
|
398
414
|
|
|
@@ -529,6 +545,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
529
545
|
shellPolicyOverrides: context.shellPolicyOverrides || null,
|
|
530
546
|
approvalGate: context.approvalGate || null,
|
|
531
547
|
shellConfirm: context.shellConfirm || null,
|
|
548
|
+
additionalDirectories: context.additionalDirectories || null,
|
|
532
549
|
});
|
|
533
550
|
} catch (err) {
|
|
534
551
|
if (hookDb) {
|
|
@@ -598,6 +615,7 @@ async function executeToolInner(
|
|
|
598
615
|
shellPolicyOverrides,
|
|
599
616
|
approvalGate,
|
|
600
617
|
shellConfirm,
|
|
618
|
+
additionalDirectories,
|
|
601
619
|
},
|
|
602
620
|
) {
|
|
603
621
|
const localToolDescriptor =
|
|
@@ -962,41 +980,52 @@ async function executeToolInner(
|
|
|
962
980
|
}
|
|
963
981
|
|
|
964
982
|
case "search_files": {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
983
|
+
// An explicit directory scopes the search to one root; otherwise span
|
|
984
|
+
// cwd plus any --add-dir roots so cross-package searches find matches.
|
|
985
|
+
const extraRoots = Array.isArray(additionalDirectories)
|
|
986
|
+
? additionalDirectories.filter(Boolean)
|
|
987
|
+
: [];
|
|
988
|
+
const roots = args.directory
|
|
989
|
+
? [path.resolve(cwd, args.directory)]
|
|
990
|
+
: [cwd, ...extraRoots];
|
|
991
|
+
const isContent = Boolean(args.content_search);
|
|
992
|
+
const cmd = isContent
|
|
993
|
+
? process.platform === "win32"
|
|
994
|
+
? `findstr /s /i /n "${args.pattern}" *`
|
|
995
|
+
: `grep -r -l -i "${args.pattern}" . --include="*" 2>/dev/null | head -20`
|
|
996
|
+
: process.platform === "win32"
|
|
997
|
+
? `dir /s /b *${args.pattern}* 2>NUL`
|
|
998
|
+
: `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
|
|
999
|
+
|
|
1000
|
+
const hits = [];
|
|
1001
|
+
const seen = new Set();
|
|
1002
|
+
for (const root of roots) {
|
|
1003
|
+
if (hits.length >= 20) break;
|
|
1004
|
+
try {
|
|
1005
|
+
if (!fs.existsSync(root)) continue;
|
|
985
1006
|
const output = execSync(cmd, {
|
|
986
|
-
cwd:
|
|
1007
|
+
cwd: root,
|
|
987
1008
|
encoding: "utf8",
|
|
988
1009
|
timeout: 10000,
|
|
989
1010
|
});
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1011
|
+
for (const line of output.trim().split("\n")) {
|
|
1012
|
+
const v = line.trim();
|
|
1013
|
+
if (!v || seen.has(v)) continue;
|
|
1014
|
+
// Qualify with the root so multi-root results stay unambiguous.
|
|
1015
|
+
const labeled = roots.length > 1 ? `${root}: ${v}` : v;
|
|
1016
|
+
seen.add(v);
|
|
1017
|
+
hits.push(labeled);
|
|
1018
|
+
if (hits.length >= 20) break;
|
|
1019
|
+
}
|
|
1020
|
+
} catch {
|
|
1021
|
+
// No matches in this root — continue to the next.
|
|
993
1022
|
}
|
|
994
|
-
} catch {
|
|
995
|
-
return attachDescriptor({
|
|
996
|
-
files: [],
|
|
997
|
-
message: "No matches found",
|
|
998
|
-
});
|
|
999
1023
|
}
|
|
1024
|
+
|
|
1025
|
+
if (hits.length === 0) {
|
|
1026
|
+
return attachDescriptor({ files: [], message: "No matches found" });
|
|
1027
|
+
}
|
|
1028
|
+
return attachDescriptor(isContent ? { matches: hits } : { files: hits });
|
|
1000
1029
|
}
|
|
1001
1030
|
|
|
1002
1031
|
case "list_dir": {
|
|
@@ -1608,9 +1637,16 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1608
1637
|
} = options;
|
|
1609
1638
|
|
|
1610
1639
|
const persona = _loadProjectPersona(options.cwd);
|
|
1640
|
+
// Merge the project-persona deny-list with any caller-supplied deny-list
|
|
1641
|
+
// (e.g. headless `--disallowed-tools`). Without this merge the caller's
|
|
1642
|
+
// deny-list is silently dropped and the tool stays callable.
|
|
1643
|
+
const mergedDisabledTools = [
|
|
1644
|
+
...(Array.isArray(persona?.toolsDisabled) ? persona.toolsDisabled : []),
|
|
1645
|
+
...(Array.isArray(options.disabledTools) ? options.disabledTools : []),
|
|
1646
|
+
];
|
|
1611
1647
|
const tools = getAgentToolDefinitions({
|
|
1612
1648
|
names: options.enabledToolNames,
|
|
1613
|
-
disabledTools:
|
|
1649
|
+
disabledTools: mergedDisabledTools,
|
|
1614
1650
|
extraTools: [
|
|
1615
1651
|
...(options.hostManagedToolPolicy?.toolDefinitions || []),
|
|
1616
1652
|
...(options.extraToolDefinitions || []),
|
|
@@ -1627,7 +1663,21 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1627
1663
|
throwIfAborted(signal);
|
|
1628
1664
|
|
|
1629
1665
|
if (provider === "ollama") {
|
|
1630
|
-
const
|
|
1666
|
+
const apiUrl = `${baseUrl}/api/chat`;
|
|
1667
|
+
// Real-time token deltas (Claude-Code `--include-partial-messages`): when
|
|
1668
|
+
// the caller supplies an onToken hook, stream the response and forward each
|
|
1669
|
+
// content chunk as it arrives. Tool calls + usage are accumulated and the
|
|
1670
|
+
// same {message, usage} shape is returned, so the agent loop is unchanged.
|
|
1671
|
+
// Without onToken we keep the cheaper single-shot non-streaming request.
|
|
1672
|
+
if (typeof options.onToken === "function") {
|
|
1673
|
+
return await _chatOllamaStreaming(
|
|
1674
|
+
apiUrl,
|
|
1675
|
+
{ model, messages, tools },
|
|
1676
|
+
options.onToken,
|
|
1677
|
+
signal,
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
const response = await fetch(apiUrl, {
|
|
1631
1681
|
method: "POST",
|
|
1632
1682
|
headers: { "Content-Type": "application/json" },
|
|
1633
1683
|
signal,
|
|
@@ -1781,6 +1831,109 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1781
1831
|
return out;
|
|
1782
1832
|
}
|
|
1783
1833
|
|
|
1834
|
+
// ─── Ollama streaming (token deltas for --include-partial-messages) ─────────
|
|
1835
|
+
//
|
|
1836
|
+
// Ollama `/api/chat` with `stream:true` returns NDJSON: one JSON object per
|
|
1837
|
+
// line, each carrying an incremental `message.content` chunk, optional
|
|
1838
|
+
// `message.tool_calls` (emitted whole, not byte-streamed), and a final line
|
|
1839
|
+
// with `done:true` + `prompt_eval_count`/`eval_count` token totals. We reduce
|
|
1840
|
+
// the stream line-by-line so onToken fires live, then finalize into the same
|
|
1841
|
+
// {message, usage} shape the non-streaming branch returns.
|
|
1842
|
+
|
|
1843
|
+
function _ollamaInitState() {
|
|
1844
|
+
return {
|
|
1845
|
+
role: "assistant",
|
|
1846
|
+
content: "",
|
|
1847
|
+
toolCalls: null,
|
|
1848
|
+
promptEval: 0,
|
|
1849
|
+
evalCount: 0,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function _ollamaReduceLine(state, line, onToken) {
|
|
1854
|
+
const s = (line || "").trim();
|
|
1855
|
+
if (!s) return state;
|
|
1856
|
+
let obj;
|
|
1857
|
+
try {
|
|
1858
|
+
obj = JSON.parse(s);
|
|
1859
|
+
} catch {
|
|
1860
|
+
return state; // tolerate partial/garbage lines mid-stream
|
|
1861
|
+
}
|
|
1862
|
+
const msg = obj.message;
|
|
1863
|
+
if (msg) {
|
|
1864
|
+
if (msg.role) state.role = msg.role;
|
|
1865
|
+
if (typeof msg.content === "string" && msg.content) {
|
|
1866
|
+
state.content += msg.content;
|
|
1867
|
+
if (typeof onToken === "function") {
|
|
1868
|
+
try {
|
|
1869
|
+
onToken(msg.content);
|
|
1870
|
+
} catch {
|
|
1871
|
+
// A failing UI hook must never break the agent run.
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
if (Array.isArray(msg.tool_calls) && msg.tool_calls.length) {
|
|
1876
|
+
state.toolCalls = (state.toolCalls || []).concat(msg.tool_calls);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (obj.prompt_eval_count) state.promptEval = obj.prompt_eval_count;
|
|
1880
|
+
if (obj.eval_count) state.evalCount = obj.eval_count;
|
|
1881
|
+
return state;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function _ollamaFinalize(state) {
|
|
1885
|
+
const message = { role: state.role, content: state.content };
|
|
1886
|
+
if (state.toolCalls && state.toolCalls.length) {
|
|
1887
|
+
message.tool_calls = state.toolCalls;
|
|
1888
|
+
}
|
|
1889
|
+
const data = { message };
|
|
1890
|
+
if (state.promptEval || state.evalCount) {
|
|
1891
|
+
data.usage = {
|
|
1892
|
+
input_tokens: state.promptEval,
|
|
1893
|
+
output_tokens: state.evalCount,
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
return data;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* Pure reducer over an iterable of Ollama NDJSON lines. Exported for tests so
|
|
1901
|
+
* the parse/accumulate logic can be exercised without a live HTTP stream.
|
|
1902
|
+
*/
|
|
1903
|
+
export function _accumulateOllamaStream(lines, onToken) {
|
|
1904
|
+
const state = _ollamaInitState();
|
|
1905
|
+
for (const line of lines) _ollamaReduceLine(state, line, onToken);
|
|
1906
|
+
return _ollamaFinalize(state);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
|
|
1910
|
+
const response = await fetch(apiUrl, {
|
|
1911
|
+
method: "POST",
|
|
1912
|
+
headers: { "Content-Type": "application/json" },
|
|
1913
|
+
signal,
|
|
1914
|
+
body: JSON.stringify({ ...body, stream: true }),
|
|
1915
|
+
});
|
|
1916
|
+
if (!response.ok) {
|
|
1917
|
+
throw new Error(`Ollama error: ${response.status}`);
|
|
1918
|
+
}
|
|
1919
|
+
const state = _ollamaInitState();
|
|
1920
|
+
const reader = response.body.getReader();
|
|
1921
|
+
const decoder = new TextDecoder();
|
|
1922
|
+
let buf = "";
|
|
1923
|
+
for (;;) {
|
|
1924
|
+
const { done, value } = await reader.read();
|
|
1925
|
+
if (done) break;
|
|
1926
|
+
buf += decoder.decode(value, { stream: true });
|
|
1927
|
+
let idx;
|
|
1928
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
1929
|
+
_ollamaReduceLine(state, buf.slice(0, idx), onToken);
|
|
1930
|
+
buf = buf.slice(idx + 1);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (buf.trim()) _ollamaReduceLine(state, buf, onToken);
|
|
1934
|
+
return _ollamaFinalize(state);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1784
1937
|
function _normalizeAnthropicResponse(data) {
|
|
1785
1938
|
const content = data.content || [];
|
|
1786
1939
|
const textBlocks = content.filter((b) => b.type === "text");
|
|
@@ -1807,11 +1960,58 @@ function _normalizeAnthropicResponse(data) {
|
|
|
1807
1960
|
|
|
1808
1961
|
// ─── Agent loop (async generator) ─────────────────────────────────────────
|
|
1809
1962
|
|
|
1963
|
+
// Tools that never mutate the workspace — auto-checkpoint skips these.
|
|
1964
|
+
const _CHECKPOINT_READ_ONLY = new Set([
|
|
1965
|
+
"read_file",
|
|
1966
|
+
"search_files",
|
|
1967
|
+
"list_dir",
|
|
1968
|
+
"list_skills",
|
|
1969
|
+
"search_sessions",
|
|
1970
|
+
]);
|
|
1971
|
+
|
|
1972
|
+
let _checkpointStoreP = null;
|
|
1973
|
+
function _loadCheckpointStore() {
|
|
1974
|
+
if (!_checkpointStoreP) {
|
|
1975
|
+
_checkpointStoreP = import("../lib/checkpoint-store.js");
|
|
1976
|
+
}
|
|
1977
|
+
return _checkpointStoreP;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
/**
|
|
1981
|
+
* Best-effort auto-checkpoint of the working tree BEFORE a mutating tool runs,
|
|
1982
|
+
* so a later `cc checkpoint restore` can roll back to just before that tool.
|
|
1983
|
+
* Enabled via toolContext.autoCheckpoint; uses the git engine only (no-op
|
|
1984
|
+
* outside a git work tree). Never throws — checkpointing must not block a tool.
|
|
1985
|
+
*
|
|
1986
|
+
* @returns {Promise<string|null>} the checkpoint id, or null when skipped
|
|
1987
|
+
*/
|
|
1988
|
+
async function _autoCheckpointBeforeTool(toolContext, toolName, toolArgs) {
|
|
1989
|
+
if (!toolContext?.autoCheckpoint) return null;
|
|
1990
|
+
if (_CHECKPOINT_READ_ONLY.has(toolName)) return null;
|
|
1991
|
+
const cwd = toolContext.cwd || process.cwd();
|
|
1992
|
+
try {
|
|
1993
|
+
const store = await _loadCheckpointStore();
|
|
1994
|
+
if (!store.isCheckpointAvailable(cwd)) return null;
|
|
1995
|
+
const res = store.createCheckpoint(cwd, {
|
|
1996
|
+
session: toolContext.checkpointSession || "agent",
|
|
1997
|
+
label: `before ${toolName}: ${formatToolArgs(toolName, toolArgs)}`.slice(
|
|
1998
|
+
0,
|
|
1999
|
+
120,
|
|
2000
|
+
),
|
|
2001
|
+
skipIfUnchanged: true,
|
|
2002
|
+
});
|
|
2003
|
+
return res?.id || null;
|
|
2004
|
+
} catch {
|
|
2005
|
+
return null; // checkpoint failure must never block the tool
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1810
2009
|
/**
|
|
1811
2010
|
* Async generator that drives the agentic tool-use loop.
|
|
1812
2011
|
*
|
|
1813
2012
|
* Yields events:
|
|
1814
2013
|
* { type: "slot-filling", slot, question } — when asking user for missing info
|
|
2014
|
+
* { type: "checkpoint", id, tool } — auto-checkpoint before a mutating tool
|
|
1815
2015
|
* { type: "tool-executing", tool, args }
|
|
1816
2016
|
* { type: "tool-result", tool, result, error }
|
|
1817
2017
|
* { type: "response-complete", content }
|
|
@@ -1819,6 +2019,38 @@ function _normalizeAnthropicResponse(data) {
|
|
|
1819
2019
|
* @param {Array} messages - mutable messages array (will be appended to)
|
|
1820
2020
|
* @param {object} options - provider, model, baseUrl, apiKey, contextEngine, hookDb, skillLoader, cwd, slotFiller, interaction
|
|
1821
2021
|
*/
|
|
2022
|
+
/**
|
|
2023
|
+
* Lazily build (and cache on `options`) the PromptCompressor used for in-loop
|
|
2024
|
+
* auto-compaction. Returns null when the feature is off or the module can't be
|
|
2025
|
+
* loaded — callers treat that as "don't compact". Cached (including null) so we
|
|
2026
|
+
* import once per run, not once per iteration.
|
|
2027
|
+
*/
|
|
2028
|
+
async function _getAutoCompactor(options) {
|
|
2029
|
+
if (Object.prototype.hasOwnProperty.call(options, "_autoCompactor")) {
|
|
2030
|
+
return options._autoCompactor;
|
|
2031
|
+
}
|
|
2032
|
+
let compressor = null;
|
|
2033
|
+
try {
|
|
2034
|
+
const { feature } = await import("../lib/feature-flags.js");
|
|
2035
|
+
if (feature("PROMPT_COMPRESSOR")) {
|
|
2036
|
+
const { PromptCompressor } =
|
|
2037
|
+
await import("../harness/prompt-compressor.js");
|
|
2038
|
+
compressor = new PromptCompressor({
|
|
2039
|
+
model: options.model,
|
|
2040
|
+
provider: options.provider,
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
} catch {
|
|
2044
|
+
compressor = null;
|
|
2045
|
+
}
|
|
2046
|
+
try {
|
|
2047
|
+
options._autoCompactor = compressor;
|
|
2048
|
+
} catch {
|
|
2049
|
+
// options may be frozen — fine, we just re-import next iteration
|
|
2050
|
+
}
|
|
2051
|
+
return compressor;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
1822
2054
|
export async function* agentLoop(messages, options) {
|
|
1823
2055
|
// Shared iteration budget — replaces hardcoded MAX_ITERATIONS.
|
|
1824
2056
|
// When options.iterationBudget is provided (e.g. from parent agent),
|
|
@@ -1842,6 +2074,10 @@ export async function* agentLoop(messages, options) {
|
|
|
1842
2074
|
shellPolicyOverrides: options.shellPolicyOverrides || null,
|
|
1843
2075
|
approvalGate: options.approvalGate || null,
|
|
1844
2076
|
shellConfirm: options.shellConfirm || null,
|
|
2077
|
+
additionalDirectories: options.additionalDirectories || null,
|
|
2078
|
+
autoCheckpoint: options.autoCheckpoint || false,
|
|
2079
|
+
checkpointSession:
|
|
2080
|
+
options.checkpointSession || options.sessionId || "agent",
|
|
1845
2081
|
};
|
|
1846
2082
|
|
|
1847
2083
|
throwIfAborted(signal);
|
|
@@ -1942,6 +2178,42 @@ export async function* agentLoop(messages, options) {
|
|
|
1942
2178
|
};
|
|
1943
2179
|
}
|
|
1944
2180
|
|
|
2181
|
+
// Headless auto-compaction (Claude-Code `--print` parity). Keeps long
|
|
2182
|
+
// `-p` / `--resume` runs under the model's context window instead of
|
|
2183
|
+
// growing until the provider rejects the request. Opt-out with
|
|
2184
|
+
// `autoCompact: false` (the interactive REPL does this — it compacts on its
|
|
2185
|
+
// own schedule). Default-on, gated by the PROMPT_COMPRESSOR flag + a size
|
|
2186
|
+
// threshold inside the compressor, so it only fires for genuinely large
|
|
2187
|
+
// contexts. Safe to compact here: the previous iteration always finishes
|
|
2188
|
+
// its full tool_call→tool_result cycle before we loop, so `messages` has no
|
|
2189
|
+
// dangling call; `preserveToolPairs` then guarantees compaction never
|
|
2190
|
+
// orphans a tool result. Best-effort — a failure never aborts the run.
|
|
2191
|
+
if (options.autoCompact !== false && messages.length > 4) {
|
|
2192
|
+
try {
|
|
2193
|
+
const compactor = await _getAutoCompactor(options);
|
|
2194
|
+
if (compactor && compactor.shouldAutoCompact(messages)) {
|
|
2195
|
+
const { messages: compacted, stats } = await compactor.compress(
|
|
2196
|
+
messages,
|
|
2197
|
+
{ preserveToolPairs: true },
|
|
2198
|
+
);
|
|
2199
|
+
if (stats.saved > 0 && compacted.length < messages.length) {
|
|
2200
|
+
messages.splice(0, messages.length, ...compacted);
|
|
2201
|
+
if (typeof options.onCompaction === "function") {
|
|
2202
|
+
try {
|
|
2203
|
+
options.onCompaction(stats, compacted);
|
|
2204
|
+
} catch {
|
|
2205
|
+
// persistence is best-effort
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
yield { type: "compaction", stats, runId };
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
} catch (_e) {
|
|
2212
|
+
if (isAbortError(_e) || signal?.aborted) throw _e;
|
|
2213
|
+
// Compaction is best-effort — proceed with the uncompacted messages.
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1945
2217
|
// Turn-scoped context injection (open-agents prepareCall parity).
|
|
1946
2218
|
// prepareCall runs fresh each iteration and returns an ephemeral
|
|
1947
2219
|
// system-message supplement that is NOT persisted to messages history.
|
|
@@ -2013,6 +2285,15 @@ export async function* agentLoop(messages, options) {
|
|
|
2013
2285
|
toolArgs = {};
|
|
2014
2286
|
}
|
|
2015
2287
|
|
|
2288
|
+
// Auto-checkpoint the work tree before a mutating tool (opt-in), so the
|
|
2289
|
+
// user can `cc checkpoint restore` back to just before this call.
|
|
2290
|
+
const cpId = await _autoCheckpointBeforeTool(
|
|
2291
|
+
toolContext,
|
|
2292
|
+
toolName,
|
|
2293
|
+
toolArgs,
|
|
2294
|
+
);
|
|
2295
|
+
if (cpId) yield { type: "checkpoint", id: cpId, tool: toolName };
|
|
2296
|
+
|
|
2016
2297
|
yield { type: "tool-executing", tool: toolName, args: toolArgs };
|
|
2017
2298
|
|
|
2018
2299
|
let toolResult;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--fallback-model` support — Claude-Code parity for unattended runs.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the agent loop's LLM call so a single request that fails with a
|
|
5
|
+
* *retryable* error (overload / rate-limit / transient network) is transparently
|
|
6
|
+
* re-issued once with a backup model. Because it sits at the chatFn seam
|
|
7
|
+
* (`agentLoop` uses `options.chatFn || chatWithTools`), fallback needs no changes
|
|
8
|
+
* to the runners — the wrapped fn is passed in via `options.chatFn`.
|
|
9
|
+
*
|
|
10
|
+
* Same-provider only: the fallback swaps `options.model`, keeping the configured
|
|
11
|
+
* provider / baseUrl / apiKey. Cross-provider fallback is a larger feature.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { chatWithTools } from "./agent-core.js";
|
|
15
|
+
|
|
16
|
+
// Heuristics for "try again on a different model" — overloaded backends,
|
|
17
|
+
// rate limits, and transient connectivity. Deliberately conservative: a 4xx
|
|
18
|
+
// that is not 429 (bad request / auth) is NOT retried.
|
|
19
|
+
const RETRYABLE_PATTERNS = [
|
|
20
|
+
/overload/i,
|
|
21
|
+
/rate.?limit/i,
|
|
22
|
+
/too many requests/i,
|
|
23
|
+
/temporarily unavailable/i,
|
|
24
|
+
/\b429\b/,
|
|
25
|
+
/\b50[0234]\b/,
|
|
26
|
+
/\b529\b/,
|
|
27
|
+
/timeout/i,
|
|
28
|
+
/timed out/i,
|
|
29
|
+
/ETIMEDOUT/i,
|
|
30
|
+
/ECONNREFUSED/i,
|
|
31
|
+
/ECONNRESET/i,
|
|
32
|
+
/ENOTFOUND/i,
|
|
33
|
+
/EAI_AGAIN/i,
|
|
34
|
+
/socket hang up/i,
|
|
35
|
+
/fetch failed/i,
|
|
36
|
+
/network error/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decide whether an error from an LLM call warrants a fallback retry.
|
|
41
|
+
* @param {any} err
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isRetryableModelError(err) {
|
|
45
|
+
if (!err) return false;
|
|
46
|
+
const status =
|
|
47
|
+
typeof err.status === "number"
|
|
48
|
+
? err.status
|
|
49
|
+
: typeof err.statusCode === "number"
|
|
50
|
+
? err.statusCode
|
|
51
|
+
: null;
|
|
52
|
+
if (status === 429) return true;
|
|
53
|
+
if (status !== null && status >= 500 && status <= 599) return true;
|
|
54
|
+
|
|
55
|
+
const parts = [
|
|
56
|
+
err.message,
|
|
57
|
+
typeof err.code === "string" ? err.code : "",
|
|
58
|
+
err.cause?.message,
|
|
59
|
+
err.cause?.code,
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(" ");
|
|
63
|
+
return RETRYABLE_PATTERNS.some((re) => re.test(parts));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a chatFn that retries once on the fallback model.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} opts
|
|
70
|
+
* @param {string} opts.fallbackModel backup model name (required)
|
|
71
|
+
* @param {Function} [opts.baseChatFn=chatWithTools] underlying LLM call
|
|
72
|
+
* @param {Function} [opts.isRetryable] error predicate (testing seam)
|
|
73
|
+
* @param {Function} [opts.onFallback] notified ({from,to,error}) on retry
|
|
74
|
+
* @returns {Function} a (messages, options) => Promise<result> chatFn
|
|
75
|
+
*/
|
|
76
|
+
export function makeFallbackChatFn(opts = {}) {
|
|
77
|
+
const fallbackModel = opts.fallbackModel;
|
|
78
|
+
const baseChatFn = opts.baseChatFn || chatWithTools;
|
|
79
|
+
const isRetryable = opts.isRetryable || isRetryableModelError;
|
|
80
|
+
const onFallback = opts.onFallback;
|
|
81
|
+
|
|
82
|
+
return async function chatWithFallback(messages, options = {}) {
|
|
83
|
+
try {
|
|
84
|
+
return await baseChatFn(messages, options);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const primaryModel = options.model;
|
|
87
|
+
// Skip a no-op retry when the fallback is the same model as the primary.
|
|
88
|
+
if (
|
|
89
|
+
!fallbackModel ||
|
|
90
|
+
fallbackModel === primaryModel ||
|
|
91
|
+
!isRetryable(err)
|
|
92
|
+
) {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
if (typeof onFallback === "function") {
|
|
96
|
+
try {
|
|
97
|
+
onFallback({
|
|
98
|
+
from: primaryModel,
|
|
99
|
+
to: fallbackModel,
|
|
100
|
+
error: err?.message || String(err),
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
// Notification is best-effort — never mask the retry.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return await baseChatFn(messages, { ...options, model: fallbackModel });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|