chainlesschain 0.162.34 → 0.162.36
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 +1 -1
- package/src/assets/web-panel/assets/{AIOps-BYfi9NYS.js → AIOps-vAVAFNJ4.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BiS_tAN7.js → ActionButton-BnRHFCKM.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-jiWl_p-B.js → Analytics-BOjwqWqG.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-m4sIzDot.js → AppLayout-Dc0D1Txn.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-CPla3Erm.js → Audit-dd_2efaZ.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BGeQzTaB.js → Backup-HF1jgm8G.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DTf7Z1iU.js → BaseInput-CCtzmoKe.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DPTlQlD-.js → Chat-BNfH1c3p.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BgRXce4e.js → ChatBubbleRenderer-DCWFqmI4.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DY-XuQMu.js → Checkbox-BOr-NscK.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B6oxPiZI.js → Codegen-DE058N7-.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Dqxb4wSE.js → Col-SOREo1XE.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DCIX514p.js → Community-sOvNZo9f.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BGtCzDoJ.js → Compact-DnBe558D.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-zcOYd55o.js → Compliance-o-r6CUbg.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DVTtdIdM.js → Cowork-D6_k9mHP.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-CPUaR69k.js → Cron-CEV3Xkrm.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DnjUS6QH.js → Crosschain-eJ1lQWKU.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Dnz8VDmx.js → DID-B-WqM9Hp.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CtWf27j7.js → Dashboard-ZnKPcsHN.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-B4GC1ZV4.js → Dropdown-B8uLWDIP.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-wjij3kzr.js → EmailListRenderer-Jmj2Y7aH.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-rS-2W4u5.js → FamilyGuardDashboard-Cb2xetG-.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-90p5Tnoz.js → Federation-C_07GXoq.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cnrw7gzq.js → FormItemContext-D3kbYrMU.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-C85NsWa3.js → GenericCardRenderer-9xgqvGPg.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BFAVM9F8.js → Git-BlwWlMMB.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DBoRonpq.js → Governance-DxN3wQZ_.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DHRyD66j.js → Inference-ls7pSw_D.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CTvUKecD.js → KnowledgeGraph-_n9hYuPI.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CB0dv_Ts.js → Logs-CvEVY5TK.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CN7Hm5Uw.js → Marketplace-C3qvQJT7.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-q5H25_8L.js → McpTools-DiwKpnKx.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BCV3pZ1d.js → Memory-CIBPi_da.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-C04Mngt4.js → MobileBridge-D-v0Se8y.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-cP1apTQD.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-ByAMz2DN.js → Mtc-BMFWrI65.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-B7V7byJq.js → MtcAudit-2s8LaHtR.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DtKmcVQV.js → Multisig-dL_nvj7d.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CaMbT5SC.js → NLProgramming-BbrJp06R.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DRjbSTCU.js → Notes-jR9irwy3.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-B9YbJID5.js → NotificationSettings-Dk-STCIX.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BcI_-vGS.js → OrderTableRenderer-CqqfY6zq.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-oTask4BE.js → Organization-BCK5jylo.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bab06ey7.js → Overflow-BRAY7Smt.js} +1 -1
- package/src/assets/web-panel/assets/{P2P--wlBeU0N.js → P2P-BltVRGjb.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-D4t77Pwc.js → PdhVaultBrowser-CV8UbXHe.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-B3sf6CJ3.js → Permissions-_tNl47Qh.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BXOojk63.js → PersonalDataHub-Cgc4HjpX.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DReqtBFN.js → Pipeline-Bn_QU4mu.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-cT1GwKLx.js → Privacy-jzJowp5P.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BhTAzVhH.js → ProjectInit-B_1pJ8qd.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CK-D8Fyj.js → ProjectSettings-CPVZpXzs.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CbHiwen6.js → Projects-CQsHOWnT.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B-ftiXa8.js → Providers-CzzMiLC0.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CT5XPwTF.js → QuickAsk-MxBKIn9o.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CohhlBZ_.js → Recommend-D8lN6Lis.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CrgbixFz.js → Reputation-CfYK-IrV.js} +1 -1
- package/src/assets/web-panel/assets/{Row-ClExmBn3.js → Row-Bg7NZDP9.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-VV0qizCJ.js → RssFeed-BOVNJhj0.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CqJapSiL.js → Search-B38qzmhY.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DY66Zie6.js → Security-CjqleZpe.js} +4 -4
- package/src/assets/web-panel/assets/{Services-RQwxat7-.js → Services-Bu9JSJap.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-0v37UTU_.js → Skeleton-B2RvRkaX.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-B4Vm4DxN.js → Skills-_h42mxMN.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CggphTlo.js → Sla-BssLs56D.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BAOU08C7.js → SpeechSettings-DCxFYHsd.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DmtC4J1w.js → SyncSettings-D2xQuNLE.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DhpOGOlo.js +1 -0
- package/src/assets/web-panel/assets/{Templates-C1QK0YoU.js → Templates-CYG-R-aS.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CieOfmqp.js → Tenant-BQRYLsvP.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DWdhrxRq.js → Terminal-imKU7N5j.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CjFVUUDU.js → TimelineRenderer-BIZzBftk.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Bwbk3id9.js → Tokens-uMLH5p_a.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-uJle_yj4.js → Trigger-BzS6XPqx.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BcOuxAA5.js → Trust-R4zhHufZ.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DUu7Ufg6.js → UkeySign-DATQCoGe.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Ck8JtQ2n.js → VideoEditing-ClUmKOtS.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B3jw43on.js → Wallet-DzJTbQzD.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Baf9K0y7.js → WebAuthn-CrXrLmzQ.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CTEDl_83.js → WorkflowEditor-CpvZ0Tma.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CKV51quV.js → chat-a6wpYmVL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BO_RP_yz.js → colors-CXJADb1t.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BZsxw_ZG.js → compact-item-CL2pohS_.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CAbvtzVL.js → createContext-xFi_1G5_.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BtmELbtB.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-QmHT8zDz.js → hasIn-Bchh1rAi.js} +1 -1
- package/src/assets/web-panel/assets/{index-fnDgExTu.js → index-B3Tpv7-d.js} +1 -1
- package/src/assets/web-panel/assets/index-B4l4vLTB.js +1 -0
- package/src/assets/web-panel/assets/{index-BEJa1FiF.js → index-B4zNisy9.js} +1 -1
- package/src/assets/web-panel/assets/{index-jd2r-T4p.js → index-B6NehWty.js} +1 -1
- package/src/assets/web-panel/assets/index-B7Ek5iiY.js +1 -0
- package/src/assets/web-panel/assets/{index-BPZHeug4.js → index-B7knYOpm.js} +1 -1
- package/src/assets/web-panel/assets/{index-GPY0LjCu.js → index-B7wT5VRi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKnngF_f.js → index-BF4xx1_b.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRNYA0BV.js → index-BH9t10pe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKquNxL2.js → index-BPH5ESqs.js} +3 -3
- package/src/assets/web-panel/assets/{index-CEh2Ry_A.js → index-BmsIKzyu.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dob6B6qS.js → index-BoaRB-4a.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ha2_56mf.js → index-BrbJBnT-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dln_vjSY.js → index-C2eMYASq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqiKnXtL.js → index-C4yBRKT4.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3fwyCjJ.js → index-CGq4HQno.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2aiE8jk.js → index-CMybtJY6.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5zhcul9.js → index-CR3kFPuC.js} +1 -1
- package/src/assets/web-panel/assets/{index-8BMLlHCv.js → index-CTRd7vkq.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6i3reUS.js → index-CdU8BwRW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNvTNZ1V.js → index-Cua_P8St.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjrDGJP2.js → index-CuehgDOp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCsZiq4i.js → index-D-TT9Swq.js} +1 -1
- package/src/assets/web-panel/assets/{index-qPafbZmr.js → index-DEYcLAl7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeC7lehI.js → index-DQ_hw_5P.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnPBG3Tr.js → index-DTEu7TSF.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8CHQnPl.js → index-DVo1GJoj.js} +1 -1
- package/src/assets/web-panel/assets/{index-9IqJODII.js → index-DjdOL159.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBCYOypV.js → index-DsbMVBj1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BL7gQAuB.js → index-DxahxRP7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC1CFfQU.js → index-EPERz4Pu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVoYeZ5Q.js → index-IkvkNxbc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsBx0u5G.js → index-KCib1PTw.js} +1 -1
- package/src/assets/web-panel/assets/{index-5hlO2-JQ.js → index-M8SZI11a.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSaI8R_7.js → index-TxbHusq2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6AA-xB2.js → index-dsLc7t6W.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRK0oAV5.js → index-jMcv1u5o.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9Z83FTS.js → index-majCS3s2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3K1eHDd.js → index-u8K1y_lh.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Bc2GWeWe.js → initDefaultProps-DYn3Gc09.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BI-Rxw6o.js → motion-ZS3eolb9.js} +1 -1
- package/src/assets/web-panel/assets/{move-DRPdwDQB.js → move-CEw4uqr3.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B4XTl3jW.js → omit-DlHFZnPp.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Do5d86Wr.js → pickAttrs-eZQvV5fA.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B8VGZ0ZF.js → placementArrow-B31jQwa-.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Cf0kI_vN.js → responsiveObserve-DAsNmVto.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Cb0psjSL.js → slide-gPQPrYZC.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Bjuo5Oal.js → statusUtils-DwWKX5co.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BLMhoHJ5.js → styleChecker-B3VOtXuH.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BdCwAfNU.js → useFlexGapSupport-6ADctM2r.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-9Jhaz5gG.js → useFs-6Zx1SSKs.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-xYFyXKwD.js → usePersonalDataHub-BzReowln.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CVhepE6Z.js → vnode-C8IpEQbD.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-IbbtJ4Zr.js → zoom-ruc9vHr0.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +161 -6
- package/src/commands/agents.js +199 -0
- package/src/commands/command.js +7 -2
- package/src/commands/hook.js +136 -28
- package/src/commands/ide.js +168 -0
- package/src/commands/mcp.js +92 -0
- package/src/commands/output-style.js +127 -0
- package/src/commands/permissions.js +211 -0
- package/src/commands/statusline.js +93 -0
- package/src/index.js +8 -0
- package/src/lib/agent-core.js +7 -0
- package/src/lib/agents.js +147 -0
- package/src/lib/hook-manager.js +1 -0
- package/src/lib/hook-runner.cjs +183 -0
- package/src/lib/ide-bridge.js +310 -0
- package/src/lib/image-input.js +156 -0
- package/src/lib/mcp-oauth.js +415 -0
- package/src/lib/output-styles.js +179 -0
- package/src/lib/permission-rules.cjs +325 -0
- package/src/lib/provider-options.js +11 -7
- package/src/lib/settings-hook-events.cjs +102 -0
- package/src/lib/settings-hooks.cjs +163 -0
- package/src/lib/settings-loader.cjs +244 -0
- package/src/lib/slash-commands.js +21 -13
- package/src/lib/status-line.cjs +204 -0
- package/src/lib/sub-agent-profiles.js +3 -0
- package/src/lib/web-search.js +487 -0
- package/src/repl/agent-repl.js +445 -35
- package/src/repl/slash-macro.js +45 -0
- package/src/runtime/agent-core.js +799 -21
- package/src/runtime/coding-agent-contract-shared.cjs +94 -4
- package/src/runtime/coding-agent-policy.cjs +24 -0
- package/src/runtime/headless-runner.js +162 -6
- package/src/runtime/headless-stream.js +133 -7
- package/src/runtime/mcp-config.js +161 -15
- package/src/runtime/policies/agent-policy.js +1 -0
- package/src/runtime/system-prompt.js +6 -1
- package/src/assets/web-panel/assets/MobileProjects-CUxONYre.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CExqxzL6.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DQYatsRR.js +0 -1
- package/src/assets/web-panel/assets/index-Bv_y1Ud7.js +0 -1
- package/src/assets/web-panel/assets/index-CZZnSJEX.js +0 -1
|
@@ -16,10 +16,15 @@
|
|
|
16
16
|
|
|
17
17
|
import fs from "fs";
|
|
18
18
|
import path from "path";
|
|
19
|
-
import { execSync } from "child_process";
|
|
19
|
+
import { execSync, spawn } from "child_process";
|
|
20
20
|
import os from "os";
|
|
21
21
|
import sharedCodingAgentPolicy from "./coding-agent-policy.cjs";
|
|
22
22
|
import sharedShellPolicy from "./coding-agent-shell-policy.cjs";
|
|
23
|
+
import sharedPermissionRules from "../lib/permission-rules.cjs";
|
|
24
|
+
import sharedSettingsHooks from "../lib/settings-hooks.cjs";
|
|
25
|
+
import sharedHookRunner from "../lib/hook-runner.cjs";
|
|
26
|
+
import sharedHookEvents from "../lib/settings-hook-events.cjs";
|
|
27
|
+
import { mergeProviderOptions } from "../lib/provider-options.js";
|
|
23
28
|
import { getPlanModeManager } from "../lib/plan-mode.js";
|
|
24
29
|
import { CLISkillLoader } from "../lib/skill-loader.js";
|
|
25
30
|
import { executeHooks, HookEvents } from "../lib/hook-manager.js";
|
|
@@ -47,6 +52,11 @@ import {
|
|
|
47
52
|
mountSkillMcpServers,
|
|
48
53
|
unmountSkillMcpServers,
|
|
49
54
|
} from "../lib/skill-mcp.js";
|
|
55
|
+
import {
|
|
56
|
+
hasImageContent,
|
|
57
|
+
toOllamaMessages,
|
|
58
|
+
imageUrlBlockToAnthropic,
|
|
59
|
+
} from "../lib/image-input.js";
|
|
50
60
|
|
|
51
61
|
/**
|
|
52
62
|
* Names of MCP servers currently mounted by an in-flight run_skill call.
|
|
@@ -62,6 +72,182 @@ export function getActiveMcpServers() {
|
|
|
62
72
|
|
|
63
73
|
const { isReadOnlyGitCommand, normalizeGitCommand } = sharedCodingAgentPolicy;
|
|
64
74
|
const { evaluateShellCommandPolicy } = sharedShellPolicy;
|
|
75
|
+
const { evaluatePermissionRules } = sharedPermissionRules;
|
|
76
|
+
const { collectHooks, umbrellaFor } = sharedSettingsHooks;
|
|
77
|
+
const { runHooks: runCommandHooks } = sharedHookRunner;
|
|
78
|
+
const { runObserveHooks } = sharedHookEvents;
|
|
79
|
+
|
|
80
|
+
// ─── Background shell tasks ────────────────────────────────────────────────
|
|
81
|
+
//
|
|
82
|
+
// run_shell is synchronous (execSync) and capped at a foreground timeout, which
|
|
83
|
+
// is the right default for quick commands but blocks the whole agent loop on
|
|
84
|
+
// long-running ones (builds, full test suites, `npm run dev`). When the model
|
|
85
|
+
// passes run_in_background:true the command is spawned instead, returns a
|
|
86
|
+
// task_id immediately, and streams its output into this registry. The agent
|
|
87
|
+
// then polls completion + incremental output via the check_shell tool — the
|
|
88
|
+
// run_in_background + BashOutput pattern from Claude Code.
|
|
89
|
+
//
|
|
90
|
+
// In-memory, process-lifetime: a task_id is only valid within the agent process
|
|
91
|
+
// that spawned it, which is exactly the polling window (one REPL session / one
|
|
92
|
+
// headless run). Buffers are bounded (MAX_BG_BUFFER per stream, tail-retained)
|
|
93
|
+
// so a chatty long task can't exhaust memory.
|
|
94
|
+
const MAX_BG_BUFFER = 1024 * 1024; // 1 MB retained tail per stream
|
|
95
|
+
const _backgroundShellTasks = new Map();
|
|
96
|
+
let _backgroundTaskSeq = 0;
|
|
97
|
+
|
|
98
|
+
function _newBgStream() {
|
|
99
|
+
return { buf: "", total: 0, dropped: 0, cursor: 0 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _appendBgStream(stream, text) {
|
|
103
|
+
stream.buf += text;
|
|
104
|
+
stream.total += text.length;
|
|
105
|
+
if (stream.buf.length > MAX_BG_BUFFER) {
|
|
106
|
+
const over = stream.buf.length - MAX_BG_BUFFER;
|
|
107
|
+
stream.buf = stream.buf.slice(over);
|
|
108
|
+
stream.dropped += over;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Read everything produced since the last read and advance the cursor. When the
|
|
113
|
+
// cursor points into a region already dropped from the retained tail, the gap
|
|
114
|
+
// is reported so the caller knows output was lost to the buffer cap.
|
|
115
|
+
function _readBgStream(stream) {
|
|
116
|
+
const bufStart = stream.total - stream.buf.length;
|
|
117
|
+
let from = stream.cursor;
|
|
118
|
+
let droppedGap = 0;
|
|
119
|
+
if (from < bufStart) {
|
|
120
|
+
droppedGap = bufStart - from;
|
|
121
|
+
from = bufStart;
|
|
122
|
+
}
|
|
123
|
+
const text = stream.buf.slice(from - bufStart);
|
|
124
|
+
stream.cursor = stream.total;
|
|
125
|
+
return { text, droppedGap };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Snapshot of background shell tasks (for REPL/host status surfaces).
|
|
130
|
+
* @returns {Array<{id:string,status:string,command:string,exitCode:number|null}>}
|
|
131
|
+
*/
|
|
132
|
+
export function listBackgroundShellTasks() {
|
|
133
|
+
return Array.from(_backgroundShellTasks.values()).map((t) => ({
|
|
134
|
+
id: t.id,
|
|
135
|
+
status: t.status,
|
|
136
|
+
command: t.command,
|
|
137
|
+
exitCode: t.exitCode,
|
|
138
|
+
startedAt: t.startedAt,
|
|
139
|
+
endedAt: t.endedAt,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Kill a background task's whole process tree. Because tasks are spawned with
|
|
144
|
+
// shell:true, the child is a shell whose real command runs as a grandchild — a
|
|
145
|
+
// plain child.kill() on POSIX only signals the shell (and often orphans the
|
|
146
|
+
// command), so a backgrounded `npm run dev` would survive. POSIX: the task is
|
|
147
|
+
// spawned detached (its own process group), so signal the group via the
|
|
148
|
+
// negative pid. Windows: `taskkill /T` walks and kills the whole tree.
|
|
149
|
+
// Returns true if a running task was signalled.
|
|
150
|
+
function _killTask(task) {
|
|
151
|
+
const child = task?.child;
|
|
152
|
+
if (!child || child.killed || task?.status !== "running") return false;
|
|
153
|
+
try {
|
|
154
|
+
if (process.platform === "win32") {
|
|
155
|
+
if (child.pid) {
|
|
156
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
157
|
+
windowsHide: true,
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
child.kill();
|
|
161
|
+
}
|
|
162
|
+
} else if (child.pid) {
|
|
163
|
+
// Negative pid → signal the whole process group (requires detached spawn).
|
|
164
|
+
try {
|
|
165
|
+
process.kill(-child.pid, "SIGTERM");
|
|
166
|
+
} catch (_err) {
|
|
167
|
+
child.kill("SIGTERM");
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
child.kill("SIGTERM");
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
} catch (_err) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Kill every still-running background shell task. Callers (REPL exit, headless
|
|
180
|
+
* shutdown) invoke this so a backgrounded `npm run dev` doesn't outlive the
|
|
181
|
+
* agent. Best-effort: kill failures are swallowed.
|
|
182
|
+
* @returns {number} count of tasks signalled
|
|
183
|
+
*/
|
|
184
|
+
export function killAllBackgroundShellTasks() {
|
|
185
|
+
let killed = 0;
|
|
186
|
+
for (const task of _backgroundShellTasks.values()) {
|
|
187
|
+
if (_killTask(task)) {
|
|
188
|
+
killed += 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return killed;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Foreground (synchronous) run_shell timeout. Configurable per-call via the
|
|
195
|
+
// optional `timeout` arg; defaults to 60s and is hard-capped at 10 min so a
|
|
196
|
+
// synchronous call can never wedge the loop indefinitely (use run_in_background
|
|
197
|
+
// for genuinely long work).
|
|
198
|
+
const DEFAULT_SHELL_TIMEOUT_MS = 60000;
|
|
199
|
+
const MAX_SHELL_TIMEOUT_MS = 600000;
|
|
200
|
+
function _resolveShellTimeout(raw) {
|
|
201
|
+
if (raw == null) return DEFAULT_SHELL_TIMEOUT_MS;
|
|
202
|
+
const n = Number(raw);
|
|
203
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_SHELL_TIMEOUT_MS;
|
|
204
|
+
return Math.min(Math.floor(n), MAX_SHELL_TIMEOUT_MS);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Run settings.json `PreToolUse` hooks (decision-capable). DB hooks are handled
|
|
209
|
+
* separately + stay observe-only. A `block` decision stops the tool; an `ask`
|
|
210
|
+
* routes to the confirmer (headless without one falls closed). spawnSync is
|
|
211
|
+
* synchronous but each hook is timeout-capped.
|
|
212
|
+
* @returns {Promise<{blocked:boolean, reason?:string, hook?:string}>}
|
|
213
|
+
*/
|
|
214
|
+
async function runSettingsPreToolUseHooks(name, args, context, cwd) {
|
|
215
|
+
const matched = collectHooks(context.settingsHooks, "PreToolUse", name);
|
|
216
|
+
if (!matched || matched.length === 0) return { blocked: false };
|
|
217
|
+
const payload = {
|
|
218
|
+
hook_event_name: "PreToolUse",
|
|
219
|
+
tool_name: umbrellaFor(name),
|
|
220
|
+
raw_tool_name: name,
|
|
221
|
+
tool_input: args,
|
|
222
|
+
cwd,
|
|
223
|
+
session_id: context.sessionId || null,
|
|
224
|
+
};
|
|
225
|
+
const outcome = runCommandHooks(matched, payload, { cwd, event: "PreToolUse" });
|
|
226
|
+
if (outcome.decision === "block") {
|
|
227
|
+
return { blocked: true, reason: outcome.reason, hook: outcome.hook };
|
|
228
|
+
}
|
|
229
|
+
if (outcome.decision === "ask") {
|
|
230
|
+
const confirm = context.permissionConfirm || context.shellConfirm || null;
|
|
231
|
+
const ok =
|
|
232
|
+
typeof confirm === "function"
|
|
233
|
+
? await confirm({
|
|
234
|
+
tool: name,
|
|
235
|
+
args,
|
|
236
|
+
rule: `hook:${outcome.hook}`,
|
|
237
|
+
reason:
|
|
238
|
+
outcome.reason || "a PreToolUse hook requests confirmation",
|
|
239
|
+
})
|
|
240
|
+
: false;
|
|
241
|
+
return ok
|
|
242
|
+
? { blocked: false }
|
|
243
|
+
: {
|
|
244
|
+
blocked: true,
|
|
245
|
+
reason: outcome.reason || "PreToolUse hook ask denied",
|
|
246
|
+
hook: outcome.hook,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { blocked: false };
|
|
250
|
+
}
|
|
65
251
|
|
|
66
252
|
// ─── Tool definitions ────────────────────────────────────────────────────
|
|
67
253
|
|
|
@@ -220,6 +406,7 @@ Key behaviors:
|
|
|
220
406
|
- When asked to modify code, read the file first, then edit it
|
|
221
407
|
- When asked to create something, use write_file to create it
|
|
222
408
|
- When asked to run/test something, use run_shell to execute it
|
|
409
|
+
- For long-running commands (builds, full test suites, dev servers) set run_shell { run_in_background: true } to get a task_id back immediately, then poll output and completion with check_shell { task_id }. Kill a backgrounded server with check_shell { task_id, kill: true } when finished
|
|
223
410
|
- When asked about git status, diff, log, or other repository operations, use the git tool instead of run_shell
|
|
224
411
|
- When asked about files or code, use read_file and search_files to find information
|
|
225
412
|
- You have multi-layer skills (built-in, marketplace, global, project-level) — use list_skills to discover them and run_skill to execute them
|
|
@@ -451,6 +638,44 @@ export async function executeTool(name, args, context = {}) {
|
|
|
451
638
|
};
|
|
452
639
|
}
|
|
453
640
|
|
|
641
|
+
// ── Permission resolution (most-restrictive-wins; denies before prompts) ──
|
|
642
|
+
// Two policy sources gate a tool call: the user's .claude/settings.json rules
|
|
643
|
+
// (deny/ask/allow) and the desktop host's synced policy (hostManagedToolPolicy,
|
|
644
|
+
// usually null in CLI). Precedence, evaluated in this exact order:
|
|
645
|
+
// 1. settings `deny` → block.
|
|
646
|
+
// 2. host `deny` → block. A settings `allow` NEVER relaxes a host deny
|
|
647
|
+
// (the desktop runtime authority outranks project
|
|
648
|
+
// config); symmetrically a settings `deny` (step 1)
|
|
649
|
+
// outranks a host `allow`. Net effect: any deny wins.
|
|
650
|
+
// 3. settings `ask` → confirm (headless w/o confirmer falls closed).
|
|
651
|
+
// Reached only after BOTH denies clear, so a denied
|
|
652
|
+
// tool never wastes a confirmation round-trip.
|
|
653
|
+
// 4. settings `allow` → pre-authorize (ruleAllowed): short-circuit the
|
|
654
|
+
// plan-mode block + run_shell ApprovalGate. The hard
|
|
655
|
+
// shell-policy denylist still applies — allow never
|
|
656
|
+
// re-enables an unsafe `rm -rf /`.
|
|
657
|
+
// No matching rule + no host policy → every existing layer runs unchanged
|
|
658
|
+
// (default behaviour is byte-for-byte).
|
|
659
|
+
const settingsVerdict = context.permissionRules
|
|
660
|
+
? evaluatePermissionRules({
|
|
661
|
+
tool: name,
|
|
662
|
+
args,
|
|
663
|
+
cwd,
|
|
664
|
+
rules: context.permissionRules,
|
|
665
|
+
})
|
|
666
|
+
: { decision: null, rule: null };
|
|
667
|
+
|
|
668
|
+
// 1. settings deny
|
|
669
|
+
if (settingsVerdict.decision === "deny") {
|
|
670
|
+
return {
|
|
671
|
+
error: `[Permission] Tool "${name}" denied by settings rule: ${settingsVerdict.rule}`,
|
|
672
|
+
policy: { decision: "deny", rule: settingsVerdict.rule, via: "settings" },
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Resolve the host policy (needed for the host-deny check + the plan-mode
|
|
677
|
+
// block below). Computed once here so a host deny can short-circuit before
|
|
678
|
+
// any settings `ask` prompt.
|
|
454
679
|
const toolPolicies =
|
|
455
680
|
context.hostManagedToolPolicy?.tools ||
|
|
456
681
|
context.hostManagedToolPolicy?.toolPolicies ||
|
|
@@ -471,6 +696,8 @@ export async function executeTool(name, args, context = {}) {
|
|
|
471
696
|
isExternalLocalTool &&
|
|
472
697
|
planManager.isActive() &&
|
|
473
698
|
localToolDescriptor?.isReadOnly === true;
|
|
699
|
+
|
|
700
|
+
// 2. host deny (a settings `allow` does not relax this)
|
|
474
701
|
if (
|
|
475
702
|
hostToolPolicy &&
|
|
476
703
|
hostToolPolicy.allowed === false &&
|
|
@@ -487,9 +714,34 @@ export async function executeTool(name, args, context = {}) {
|
|
|
487
714
|
};
|
|
488
715
|
}
|
|
489
716
|
|
|
490
|
-
//
|
|
717
|
+
// 3 + 4. settings ask / allow (only reached when neither layer denied)
|
|
718
|
+
let ruleAllowed = false;
|
|
719
|
+
if (settingsVerdict.decision === "ask") {
|
|
720
|
+
const confirm = context.permissionConfirm || context.shellConfirm || null;
|
|
721
|
+
const ok =
|
|
722
|
+
typeof confirm === "function"
|
|
723
|
+
? await confirm({
|
|
724
|
+
tool: name,
|
|
725
|
+
args,
|
|
726
|
+
rule: settingsVerdict.rule,
|
|
727
|
+
reason: `settings rule ${settingsVerdict.rule} requires confirmation`,
|
|
728
|
+
})
|
|
729
|
+
: false;
|
|
730
|
+
if (!ok) {
|
|
731
|
+
return {
|
|
732
|
+
error: `[Permission] Tool "${name}" requires confirmation (settings rule: ${settingsVerdict.rule}) — denied.`,
|
|
733
|
+
policy: { decision: "ask", rule: settingsVerdict.rule, via: "settings" },
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
ruleAllowed = true; // confirmed → treat like allow downstream
|
|
737
|
+
} else if (settingsVerdict.decision === "allow") {
|
|
738
|
+
ruleAllowed = true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Plan mode: check if tool is allowed (a settings `allow` rule pre-authorizes)
|
|
491
742
|
if (
|
|
492
743
|
planManager.isActive() &&
|
|
744
|
+
!ruleAllowed &&
|
|
493
745
|
!(name === "git" && isReadOnlyGitCommand(args.command)) &&
|
|
494
746
|
!planManager.isToolAllowed(name) &&
|
|
495
747
|
!(isExternalHostTool && hostToolPolicy?.allowed === true) &&
|
|
@@ -514,7 +766,11 @@ export async function executeTool(name, args, context = {}) {
|
|
|
514
766
|
};
|
|
515
767
|
}
|
|
516
768
|
|
|
517
|
-
// PreToolUse hook
|
|
769
|
+
// PreToolUse hooks. DB hooks (cc hook add) stay observe-only — a failure
|
|
770
|
+
// never blocks. settings.json hooks (context.settingsHooks) are decision-
|
|
771
|
+
// capable: a `block` (exit 2 / {decision:block}) stops the tool here, an
|
|
772
|
+
// `ask` routes to the confirmer. Runs after permission resolution so a
|
|
773
|
+
// settings deny / host deny short-circuits before any hook process spawns.
|
|
518
774
|
if (hookDb) {
|
|
519
775
|
try {
|
|
520
776
|
await executeHooks(hookDb, HookEvents.PreToolUse, {
|
|
@@ -528,6 +784,15 @@ export async function executeTool(name, args, context = {}) {
|
|
|
528
784
|
// Hook failure should not block tool execution
|
|
529
785
|
}
|
|
530
786
|
}
|
|
787
|
+
if (context.settingsHooks) {
|
|
788
|
+
const pre = await runSettingsPreToolUseHooks(name, args, context, cwd);
|
|
789
|
+
if (pre.blocked) {
|
|
790
|
+
return {
|
|
791
|
+
error: `[Hook] PreToolUse blocked "${name}"${pre.reason ? ": " + pre.reason : ""}`,
|
|
792
|
+
policy: { decision: "block", via: "hook", hook: pre.hook || null },
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
}
|
|
531
796
|
|
|
532
797
|
const startTime = Date.now();
|
|
533
798
|
let toolResult;
|
|
@@ -546,6 +811,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
546
811
|
approvalGate: context.approvalGate || null,
|
|
547
812
|
shellConfirm: context.shellConfirm || null,
|
|
548
813
|
additionalDirectories: context.additionalDirectories || null,
|
|
814
|
+
ruleAllowed,
|
|
549
815
|
});
|
|
550
816
|
} catch (err) {
|
|
551
817
|
if (hookDb) {
|
|
@@ -592,6 +858,36 @@ export async function executeTool(name, args, context = {}) {
|
|
|
592
858
|
// Non-critical
|
|
593
859
|
}
|
|
594
860
|
}
|
|
861
|
+
// settings.json PostToolUse hooks: can't un-run the tool, but a `block`
|
|
862
|
+
// reason is attached as `hookFeedback` to be surfaced back to the model.
|
|
863
|
+
if (context.settingsHooks && toolResult && typeof toolResult === "object") {
|
|
864
|
+
try {
|
|
865
|
+
const matched = collectHooks(context.settingsHooks, "PostToolUse", name);
|
|
866
|
+
if (matched && matched.length > 0) {
|
|
867
|
+
const outcome = runCommandHooks(
|
|
868
|
+
matched,
|
|
869
|
+
{
|
|
870
|
+
hook_event_name: "PostToolUse",
|
|
871
|
+
tool_name: umbrellaFor(name),
|
|
872
|
+
raw_tool_name: name,
|
|
873
|
+
tool_input: args,
|
|
874
|
+
tool_response:
|
|
875
|
+
typeof toolResult === "object"
|
|
876
|
+
? JSON.stringify(toolResult).substring(0, 2000)
|
|
877
|
+
: String(toolResult).substring(0, 2000),
|
|
878
|
+
cwd,
|
|
879
|
+
session_id: context.sessionId || null,
|
|
880
|
+
},
|
|
881
|
+
{ cwd, event: "PostToolUse" },
|
|
882
|
+
);
|
|
883
|
+
if (outcome.decision === "block" && outcome.reason) {
|
|
884
|
+
toolResult.hookFeedback = outcome.reason;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
} catch (_err) {
|
|
888
|
+
// PostToolUse hooks are best-effort
|
|
889
|
+
}
|
|
890
|
+
}
|
|
595
891
|
|
|
596
892
|
return toolResult;
|
|
597
893
|
}
|
|
@@ -612,10 +908,12 @@ async function executeToolInner(
|
|
|
612
908
|
externalToolDescriptors,
|
|
613
909
|
externalToolExecutors,
|
|
614
910
|
mcpClient,
|
|
911
|
+
llmOptions,
|
|
615
912
|
shellPolicyOverrides,
|
|
616
913
|
approvalGate,
|
|
617
914
|
shellConfirm,
|
|
618
915
|
additionalDirectories,
|
|
916
|
+
ruleAllowed = false,
|
|
619
917
|
},
|
|
620
918
|
) {
|
|
621
919
|
const localToolDescriptor =
|
|
@@ -760,7 +1058,10 @@ async function executeToolInner(
|
|
|
760
1058
|
const override = getRuntimeToolDescriptorByCommand(args.command);
|
|
761
1059
|
let shellPolicy;
|
|
762
1060
|
let approvalOutcome = null;
|
|
763
|
-
|
|
1061
|
+
// A settings `allow` rule (ruleAllowed) pre-authorizes: skip the
|
|
1062
|
+
// ApprovalGate tier confirm, but still run the hard shell-policy denylist
|
|
1063
|
+
// below so an allow rule can never re-enable a blocked unsafe command.
|
|
1064
|
+
if (approvalGate && !ruleAllowed) {
|
|
764
1065
|
const { evaluateShellCommandWithApproval } =
|
|
765
1066
|
await import("../lib/shell-approval.js");
|
|
766
1067
|
const gated = await evaluateShellCommandWithApproval({
|
|
@@ -802,11 +1103,86 @@ async function executeToolInner(
|
|
|
802
1103
|
}
|
|
803
1104
|
}
|
|
804
1105
|
|
|
1106
|
+
// Background: spawn, register, return a task_id immediately. The agent
|
|
1107
|
+
// polls output + completion via check_shell. No timeout — that's the whole
|
|
1108
|
+
// point of backgrounding (builds, test suites, dev servers).
|
|
1109
|
+
if (args.run_in_background === true) {
|
|
1110
|
+
const id = `bg_${++_backgroundTaskSeq}`;
|
|
1111
|
+
const task = {
|
|
1112
|
+
id,
|
|
1113
|
+
command: args.command,
|
|
1114
|
+
cwd: args.cwd || cwd,
|
|
1115
|
+
status: "running",
|
|
1116
|
+
exitCode: null,
|
|
1117
|
+
signal: null,
|
|
1118
|
+
error: null,
|
|
1119
|
+
startedAt: new Date().toISOString(),
|
|
1120
|
+
endedAt: null,
|
|
1121
|
+
out: _newBgStream(),
|
|
1122
|
+
err: _newBgStream(),
|
|
1123
|
+
child: null,
|
|
1124
|
+
};
|
|
1125
|
+
try {
|
|
1126
|
+
const child = spawn(args.command, {
|
|
1127
|
+
cwd: task.cwd,
|
|
1128
|
+
shell: true,
|
|
1129
|
+
windowsHide: true,
|
|
1130
|
+
// POSIX: own process group so check_shell{kill}/teardown can signal
|
|
1131
|
+
// the whole tree (shell + its grandchild command). No-op on Windows
|
|
1132
|
+
// where the tree is killed via taskkill /T instead.
|
|
1133
|
+
detached: process.platform !== "win32",
|
|
1134
|
+
});
|
|
1135
|
+
task.child = child;
|
|
1136
|
+
if (child.stdout) {
|
|
1137
|
+
child.stdout.setEncoding("utf8");
|
|
1138
|
+
child.stdout.on("data", (d) => _appendBgStream(task.out, d));
|
|
1139
|
+
}
|
|
1140
|
+
if (child.stderr) {
|
|
1141
|
+
child.stderr.setEncoding("utf8");
|
|
1142
|
+
child.stderr.on("data", (d) => _appendBgStream(task.err, d));
|
|
1143
|
+
}
|
|
1144
|
+
child.on("error", (err) => {
|
|
1145
|
+
task.status = "error";
|
|
1146
|
+
task.error = String(err?.message || err).substring(0, 2000);
|
|
1147
|
+
task.endedAt = new Date().toISOString();
|
|
1148
|
+
});
|
|
1149
|
+
// 'close' (not 'exit') so stdout/stderr are fully drained before the
|
|
1150
|
+
// status leaves "running" — otherwise a poll can observe completion
|
|
1151
|
+
// with the final output chunk not yet buffered.
|
|
1152
|
+
child.on("close", (code, signal) => {
|
|
1153
|
+
// 'error' may have already set a terminal state; don't overwrite it.
|
|
1154
|
+
if (task.status === "running") {
|
|
1155
|
+
task.status = code === 0 ? "exited" : "failed";
|
|
1156
|
+
}
|
|
1157
|
+
task.exitCode = code;
|
|
1158
|
+
task.signal = signal;
|
|
1159
|
+
task.endedAt = new Date().toISOString();
|
|
1160
|
+
});
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
task.status = "error";
|
|
1163
|
+
task.error = String(err?.message || err).substring(0, 2000);
|
|
1164
|
+
task.endedAt = new Date().toISOString();
|
|
1165
|
+
}
|
|
1166
|
+
_backgroundShellTasks.set(id, task);
|
|
1167
|
+
return attachDescriptor(
|
|
1168
|
+
{
|
|
1169
|
+
background: true,
|
|
1170
|
+
task_id: id,
|
|
1171
|
+
status: task.status,
|
|
1172
|
+
command: task.command,
|
|
1173
|
+
hint: "Poll output and completion with the check_shell tool using this task_id. Kill long-lived servers with check_shell { task_id, kill: true } when done.",
|
|
1174
|
+
shellCommandPolicy: shellPolicy,
|
|
1175
|
+
approval: approvalOutcome,
|
|
1176
|
+
},
|
|
1177
|
+
override || runtimeDescriptor,
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
805
1181
|
try {
|
|
806
1182
|
const output = execSync(args.command, {
|
|
807
1183
|
cwd: args.cwd || cwd,
|
|
808
1184
|
encoding: "utf8",
|
|
809
|
-
timeout:
|
|
1185
|
+
timeout: _resolveShellTimeout(args.timeout),
|
|
810
1186
|
maxBuffer: 1024 * 1024,
|
|
811
1187
|
});
|
|
812
1188
|
return attachDescriptor(
|
|
@@ -831,6 +1207,51 @@ async function executeToolInner(
|
|
|
831
1207
|
}
|
|
832
1208
|
}
|
|
833
1209
|
|
|
1210
|
+
case "check_shell": {
|
|
1211
|
+
const taskId = args.task_id;
|
|
1212
|
+
// No task_id → list known background tasks (lightweight status surface).
|
|
1213
|
+
if (!taskId) {
|
|
1214
|
+
return attachDescriptor({
|
|
1215
|
+
tasks: listBackgroundShellTasks(),
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
const task = _backgroundShellTasks.get(taskId);
|
|
1219
|
+
if (!task) {
|
|
1220
|
+
return attachDescriptor({
|
|
1221
|
+
error: `No background shell task with id "${taskId}".`,
|
|
1222
|
+
tasks: listBackgroundShellTasks(),
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
let killed = false;
|
|
1226
|
+
if (args.kill === true) {
|
|
1227
|
+
// _killTask signals the whole process tree (see its doc); the close
|
|
1228
|
+
// handler flips status. Best-effort.
|
|
1229
|
+
killed = _killTask(task);
|
|
1230
|
+
}
|
|
1231
|
+
const out = _readBgStream(task.out);
|
|
1232
|
+
const err = _readBgStream(task.err);
|
|
1233
|
+
return attachDescriptor({
|
|
1234
|
+
task_id: task.id,
|
|
1235
|
+
status: task.status,
|
|
1236
|
+
running: task.status === "running",
|
|
1237
|
+
command: task.command,
|
|
1238
|
+
exitCode: task.exitCode,
|
|
1239
|
+
signal: task.signal,
|
|
1240
|
+
...(task.error ? { error: task.error } : {}),
|
|
1241
|
+
stdout: out.text.substring(0, 30000),
|
|
1242
|
+
stderr: err.text.substring(0, 30000),
|
|
1243
|
+
...(out.droppedGap
|
|
1244
|
+
? { stdout_dropped_bytes: out.droppedGap }
|
|
1245
|
+
: {}),
|
|
1246
|
+
...(err.droppedGap
|
|
1247
|
+
? { stderr_dropped_bytes: err.droppedGap }
|
|
1248
|
+
: {}),
|
|
1249
|
+
...(killed ? { killed: true } : {}),
|
|
1250
|
+
startedAt: task.startedAt,
|
|
1251
|
+
endedAt: task.endedAt,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
834
1255
|
case "git": {
|
|
835
1256
|
const normalizedCommand = normalizeGitCommand(args.command);
|
|
836
1257
|
if (!normalizedCommand) {
|
|
@@ -874,6 +1295,7 @@ async function executeToolInner(
|
|
|
874
1295
|
parentMessages,
|
|
875
1296
|
interaction,
|
|
876
1297
|
sessionId,
|
|
1298
|
+
llmOptions,
|
|
877
1299
|
}),
|
|
878
1300
|
);
|
|
879
1301
|
}
|
|
@@ -905,6 +1327,33 @@ async function executeToolInner(
|
|
|
905
1327
|
}
|
|
906
1328
|
}
|
|
907
1329
|
|
|
1330
|
+
case "web_search": {
|
|
1331
|
+
try {
|
|
1332
|
+
const { webSearch } = await import("../lib/web-search.js");
|
|
1333
|
+
let webSearchConfig = {};
|
|
1334
|
+
try {
|
|
1335
|
+
const { loadProjectConfig: _lpc, findProjectRoot: _fpr } =
|
|
1336
|
+
await import("../lib/project-detector.js");
|
|
1337
|
+
const projectRoot = _fpr(cwd);
|
|
1338
|
+
if (projectRoot) {
|
|
1339
|
+
const cfg = _lpc(projectRoot);
|
|
1340
|
+
webSearchConfig = cfg?.webSearch || {};
|
|
1341
|
+
}
|
|
1342
|
+
} catch (_err) {
|
|
1343
|
+
// Config optional — use defaults (auto provider / keyless fallback)
|
|
1344
|
+
}
|
|
1345
|
+
const result = await webSearch(args.query, {
|
|
1346
|
+
provider: args.provider,
|
|
1347
|
+
maxResults: args.maxResults,
|
|
1348
|
+
timeout: args.timeout,
|
|
1349
|
+
config: webSearchConfig,
|
|
1350
|
+
});
|
|
1351
|
+
return attachDescriptor(result);
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
return attachDescriptor({ error: `web_search failed: ${err.message}` });
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
908
1357
|
case "todo_write": {
|
|
909
1358
|
try {
|
|
910
1359
|
const { writeTodos } = await import("../lib/todo-manager.js");
|
|
@@ -1470,7 +1919,7 @@ async function _executeRunCode(args, cwd) {
|
|
|
1470
1919
|
* @returns {Promise<object>}
|
|
1471
1920
|
*/
|
|
1472
1921
|
async function _executeSpawnSubAgent(args, ctx) {
|
|
1473
|
-
|
|
1922
|
+
let {
|
|
1474
1923
|
role,
|
|
1475
1924
|
task,
|
|
1476
1925
|
context: inheritedContext,
|
|
@@ -1478,8 +1927,37 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1478
1927
|
profile: profileName,
|
|
1479
1928
|
} = args;
|
|
1480
1929
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1930
|
+
// Named subagent delegation (cc agents / .chainlesschain|.claude/agents/*.md):
|
|
1931
|
+
// load the agent's persona (its body = system prompt) + tool allow-list.
|
|
1932
|
+
// Explicit role/tools still win over the agent file's values.
|
|
1933
|
+
let mdProfile = null;
|
|
1934
|
+
let mdModel = null;
|
|
1935
|
+
if (args.agent) {
|
|
1936
|
+
try {
|
|
1937
|
+
const { getAgent } = await import("../lib/agents.js");
|
|
1938
|
+
const md = getAgent(args.agent, ctx.cwd);
|
|
1939
|
+
if (!md) {
|
|
1940
|
+
return {
|
|
1941
|
+
error: `Unknown subagent "${args.agent}". List them with: cc agents list`,
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
role = role || md.name;
|
|
1945
|
+
if (!explicitTools && Array.isArray(md.tools)) explicitTools = md.tools;
|
|
1946
|
+
if (md.model) mdModel = md.model;
|
|
1947
|
+
if (md.systemPrompt) {
|
|
1948
|
+
mdProfile = { name: md.name, systemPrompt: md.systemPrompt };
|
|
1949
|
+
}
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
return {
|
|
1952
|
+
error: `Failed to load subagent "${args.agent}": ${err.message}`,
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (!task || (!role && !args.agent)) {
|
|
1958
|
+
return {
|
|
1959
|
+
error: "spawn_sub_agent requires 'task' and either 'role' or 'agent'",
|
|
1960
|
+
};
|
|
1483
1961
|
}
|
|
1484
1962
|
|
|
1485
1963
|
// Phase 3: resolve declarative profile if requested. Explicit tools/context
|
|
@@ -1500,6 +1978,10 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1500
1978
|
}
|
|
1501
1979
|
}
|
|
1502
1980
|
|
|
1981
|
+
// A named subagent's body becomes the sub-agent system prompt (via the
|
|
1982
|
+
// profile.systemPrompt seam) when no declarative profile was requested.
|
|
1983
|
+
if (!profile && mdProfile) profile = mdProfile;
|
|
1984
|
+
|
|
1503
1985
|
const allowedTools = Array.isArray(explicitTools)
|
|
1504
1986
|
? explicitTools
|
|
1505
1987
|
: profile?.toolAllowlist || null;
|
|
@@ -1521,6 +2003,14 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1521
2003
|
const parentSessionId = ctx.sessionId || null;
|
|
1522
2004
|
const interaction = ctx.interaction || null;
|
|
1523
2005
|
|
|
2006
|
+
// Inherit the parent's provider / base-url / key; a named subagent's `model:`
|
|
2007
|
+
// frontmatter (mdModel) overrides just the model, else keep the parent's.
|
|
2008
|
+
const parentLlm = ctx.llmOptions || {};
|
|
2009
|
+
const subLlmOptions = {
|
|
2010
|
+
...parentLlm,
|
|
2011
|
+
model: mdModel || parentLlm.model || undefined,
|
|
2012
|
+
};
|
|
2013
|
+
|
|
1524
2014
|
const subCtx = SubAgentContext.create({
|
|
1525
2015
|
role,
|
|
1526
2016
|
task,
|
|
@@ -1529,6 +2019,7 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1529
2019
|
allowedTools: allowedTools || null,
|
|
1530
2020
|
cwd: ctx.cwd,
|
|
1531
2021
|
profile: profile || null,
|
|
2022
|
+
llmOptions: subLlmOptions,
|
|
1532
2023
|
});
|
|
1533
2024
|
|
|
1534
2025
|
const emit = (type, payload) => {
|
|
@@ -1664,6 +2155,12 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1664
2155
|
|
|
1665
2156
|
if (provider === "ollama") {
|
|
1666
2157
|
const apiUrl = `${baseUrl}/api/chat`;
|
|
2158
|
+
// Multimodal (`cc agent --image`): ollama wants `{content, images:[base64]}`
|
|
2159
|
+
// not OpenAI-style `image_url` blocks. Convert only when an image part is
|
|
2160
|
+
// present so text-only runs keep the identical request shape.
|
|
2161
|
+
const ollamaMessages = hasImageContent(messages)
|
|
2162
|
+
? toOllamaMessages(messages)
|
|
2163
|
+
: messages;
|
|
1667
2164
|
// Real-time token deltas (Claude-Code `--include-partial-messages`): when
|
|
1668
2165
|
// the caller supplies an onToken hook, stream the response and forward each
|
|
1669
2166
|
// content chunk as it arrives. Tool calls + usage are accumulated and the
|
|
@@ -1672,7 +2169,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1672
2169
|
if (typeof options.onToken === "function") {
|
|
1673
2170
|
return await _chatOllamaStreaming(
|
|
1674
2171
|
apiUrl,
|
|
1675
|
-
{ model, messages, tools },
|
|
2172
|
+
{ model, messages: ollamaMessages, tools },
|
|
1676
2173
|
options.onToken,
|
|
1677
2174
|
signal,
|
|
1678
2175
|
);
|
|
@@ -1683,7 +2180,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1683
2180
|
signal,
|
|
1684
2181
|
body: JSON.stringify({
|
|
1685
2182
|
model,
|
|
1686
|
-
messages,
|
|
2183
|
+
messages: ollamaMessages,
|
|
1687
2184
|
tools,
|
|
1688
2185
|
stream: false,
|
|
1689
2186
|
}),
|
|
@@ -1714,12 +2211,35 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1714
2211
|
input_schema: t.function.parameters,
|
|
1715
2212
|
}));
|
|
1716
2213
|
|
|
2214
|
+
// Model-aware max_tokens (Opus → 16384, Haiku → 4096, else 8192) via
|
|
2215
|
+
// provider-options. We read ONLY maxTokens: the module's `temperature`
|
|
2216
|
+
// default is never forwarded (400s on Opus 4.7/4.8).
|
|
2217
|
+
const effModel = model || "claude-sonnet-4-20250514";
|
|
2218
|
+
const { maxTokens: anthropicMaxTokens } = mergeProviderOptions(
|
|
2219
|
+
"anthropic",
|
|
2220
|
+
effModel,
|
|
2221
|
+
);
|
|
1717
2222
|
const body = {
|
|
1718
|
-
model:
|
|
1719
|
-
max_tokens: 8192,
|
|
1720
|
-
|
|
2223
|
+
model: effModel,
|
|
2224
|
+
max_tokens: anthropicMaxTokens || 8192,
|
|
2225
|
+
// Convert cc's internal OpenAI-shaped history (role:"tool" results,
|
|
2226
|
+
// assistant tool_calls[]) into Anthropic content blocks. Without this,
|
|
2227
|
+
// multi-turn tool use 400s on turn 2 (Anthropic rejects role:"tool" and
|
|
2228
|
+
// assistant `tool_calls`). Also replays preserved thinking blocks.
|
|
2229
|
+
messages: _toAnthropicMessages(otherMsgs),
|
|
1721
2230
|
tools: anthropicTools,
|
|
1722
2231
|
};
|
|
2232
|
+
// Extended thinking — OPT-IN via options.thinking; off by default so the
|
|
2233
|
+
// request is byte-identical to before. Model-aware (adaptive+effort on Opus
|
|
2234
|
+
// 4.6+/Sonnet 4.6, legacy enabled+budget else; nothing on Haiku). temperature
|
|
2235
|
+
// is never sent. RUNTIME-UNVERIFIED: no Anthropic key here to E2E the
|
|
2236
|
+
// thinking-block signature replay (see cli_claude_code_parity_landed memory).
|
|
2237
|
+
const thinkingParams = _anthropicThinkingParams(
|
|
2238
|
+
effModel,
|
|
2239
|
+
options,
|
|
2240
|
+
body.max_tokens,
|
|
2241
|
+
);
|
|
2242
|
+
if (thinkingParams) Object.assign(body, thinkingParams);
|
|
1723
2243
|
if (systemMsgs.length > 0) {
|
|
1724
2244
|
body.system = systemMsgs.map((m) => m.content).join("\n");
|
|
1725
2245
|
}
|
|
@@ -1999,7 +2519,11 @@ function _anthropicReduceLine(state, raw, onToken) {
|
|
|
1999
2519
|
state.blocks[obj.index] =
|
|
2000
2520
|
cb.type === "tool_use"
|
|
2001
2521
|
? { type: "tool_use", id: cb.id, name: cb.name, json: "" }
|
|
2002
|
-
:
|
|
2522
|
+
: cb.type === "thinking"
|
|
2523
|
+
? { type: "thinking", thinking: "", signature: "" }
|
|
2524
|
+
: cb.type === "redacted_thinking"
|
|
2525
|
+
? { type: "redacted_thinking", data: cb.data || "" }
|
|
2526
|
+
: { type: "text" };
|
|
2003
2527
|
} else if (obj.type === "content_block_delta") {
|
|
2004
2528
|
const d = obj.delta || {};
|
|
2005
2529
|
if (d.type === "text_delta" && d.text) {
|
|
@@ -2013,6 +2537,12 @@ function _anthropicReduceLine(state, raw, onToken) {
|
|
|
2013
2537
|
}
|
|
2014
2538
|
} else if (d.type === "input_json_delta" && state.blocks[obj.index]) {
|
|
2015
2539
|
state.blocks[obj.index].json += d.partial_json || "";
|
|
2540
|
+
} else if (d.type === "thinking_delta" && state.blocks[obj.index]) {
|
|
2541
|
+
state.blocks[obj.index].thinking =
|
|
2542
|
+
(state.blocks[obj.index].thinking || "") + (d.thinking || "");
|
|
2543
|
+
} else if (d.type === "signature_delta" && state.blocks[obj.index]) {
|
|
2544
|
+
state.blocks[obj.index].signature =
|
|
2545
|
+
(state.blocks[obj.index].signature || "") + (d.signature || "");
|
|
2016
2546
|
}
|
|
2017
2547
|
} else if (obj.type === "message_delta") {
|
|
2018
2548
|
state.outputTokens = Number(obj.usage?.output_tokens) || state.outputTokens;
|
|
@@ -2022,7 +2552,10 @@ function _anthropicReduceLine(state, raw, onToken) {
|
|
|
2022
2552
|
|
|
2023
2553
|
function _anthropicFinalize(state) {
|
|
2024
2554
|
const toolCalls = [];
|
|
2025
|
-
|
|
2555
|
+
const thinkingBlocks = [];
|
|
2556
|
+
for (const k of Object.keys(state.blocks).sort(
|
|
2557
|
+
(a, b) => Number(a) - Number(b),
|
|
2558
|
+
)) {
|
|
2026
2559
|
const b = state.blocks[k];
|
|
2027
2560
|
if (b.type === "tool_use") {
|
|
2028
2561
|
let input = {};
|
|
@@ -2036,10 +2569,21 @@ function _anthropicFinalize(state) {
|
|
|
2036
2569
|
type: "function",
|
|
2037
2570
|
function: { name: b.name, arguments: JSON.stringify(input) },
|
|
2038
2571
|
});
|
|
2572
|
+
} else if (b.type === "thinking") {
|
|
2573
|
+
thinkingBlocks.push({
|
|
2574
|
+
type: "thinking",
|
|
2575
|
+
thinking: b.thinking || "",
|
|
2576
|
+
signature: b.signature || "",
|
|
2577
|
+
});
|
|
2578
|
+
} else if (b.type === "redacted_thinking") {
|
|
2579
|
+
thinkingBlocks.push({ type: "redacted_thinking", data: b.data || "" });
|
|
2039
2580
|
}
|
|
2040
2581
|
}
|
|
2041
2582
|
const message = { role: "assistant", content: state.text };
|
|
2042
2583
|
if (toolCalls.length) message.tool_calls = toolCalls;
|
|
2584
|
+
// Preserve thinking blocks verbatim (incl. signature) for replay on the next
|
|
2585
|
+
// tool turn — required by the API when extended thinking is on.
|
|
2586
|
+
if (thinkingBlocks.length) message._thinkingBlocks = thinkingBlocks;
|
|
2043
2587
|
const out = { message };
|
|
2044
2588
|
if (state.inputTokens || state.outputTokens) {
|
|
2045
2589
|
out.usage = {
|
|
@@ -2187,10 +2731,154 @@ async function _chatOpenAIStreaming(apiUrl, body, apiKey, onToken, signal, provi
|
|
|
2187
2731
|
return _openaiFinalize(state);
|
|
2188
2732
|
}
|
|
2189
2733
|
|
|
2190
|
-
|
|
2734
|
+
/**
|
|
2735
|
+
* Convert cc's internal OpenAI-shaped messages into Anthropic content-block
|
|
2736
|
+
* messages. Internal shape: {role:"user"|"assistant"|"tool", content,
|
|
2737
|
+
* tool_calls?, _thinkingBlocks?}. Anthropic shape: {role:"user"|"assistant",
|
|
2738
|
+
* content: string | block[]} — assistant tool calls become {type:"tool_use"}
|
|
2739
|
+
* blocks; tool results become {type:"tool_result"} blocks inside a USER turn,
|
|
2740
|
+
* with consecutive results merged. Preserved thinking blocks (with signature)
|
|
2741
|
+
* are replayed FIRST in the assistant turn (the API requires them ahead of
|
|
2742
|
+
* tool_use when continuing a thinking+tool turn). Exported for tests.
|
|
2743
|
+
*/
|
|
2744
|
+
export function _toAnthropicMessages(msgs) {
|
|
2745
|
+
const out = [];
|
|
2746
|
+
let pendingResults = [];
|
|
2747
|
+
const flush = () => {
|
|
2748
|
+
if (pendingResults.length) {
|
|
2749
|
+
out.push({ role: "user", content: pendingResults });
|
|
2750
|
+
pendingResults = [];
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
for (const m of msgs || []) {
|
|
2754
|
+
if (!m) continue;
|
|
2755
|
+
if (m.role === "tool") {
|
|
2756
|
+
pendingResults.push({
|
|
2757
|
+
type: "tool_result",
|
|
2758
|
+
tool_use_id: m.tool_call_id,
|
|
2759
|
+
content:
|
|
2760
|
+
typeof m.content === "string"
|
|
2761
|
+
? m.content
|
|
2762
|
+
: JSON.stringify(m.content ?? ""),
|
|
2763
|
+
});
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
flush();
|
|
2767
|
+
if (m.role === "assistant") {
|
|
2768
|
+
const blocks = [];
|
|
2769
|
+
if (Array.isArray(m._thinkingBlocks)) {
|
|
2770
|
+
for (const tb of m._thinkingBlocks) blocks.push(tb);
|
|
2771
|
+
}
|
|
2772
|
+
if (typeof m.content === "string" && m.content.trim()) {
|
|
2773
|
+
blocks.push({ type: "text", text: m.content });
|
|
2774
|
+
} else if (Array.isArray(m.content)) {
|
|
2775
|
+
for (const b of m.content) blocks.push(b);
|
|
2776
|
+
}
|
|
2777
|
+
if (Array.isArray(m.tool_calls)) {
|
|
2778
|
+
for (const tc of m.tool_calls) {
|
|
2779
|
+
const raw = tc.function?.arguments;
|
|
2780
|
+
let input = {};
|
|
2781
|
+
try {
|
|
2782
|
+
input =
|
|
2783
|
+
typeof raw === "string" ? JSON.parse(raw || "{}") : raw || {};
|
|
2784
|
+
} catch {
|
|
2785
|
+
input = {};
|
|
2786
|
+
}
|
|
2787
|
+
blocks.push({
|
|
2788
|
+
type: "tool_use",
|
|
2789
|
+
id: tc.id,
|
|
2790
|
+
name: tc.function?.name,
|
|
2791
|
+
input,
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
out.push({
|
|
2796
|
+
role: "assistant",
|
|
2797
|
+
content: blocks.length ? blocks : m.content || "",
|
|
2798
|
+
});
|
|
2799
|
+
} else {
|
|
2800
|
+
// user turn: pass content through (string or already-block array). When
|
|
2801
|
+
// it carries OpenAI-style `image_url` parts (`cc agent --image`), convert
|
|
2802
|
+
// them to Anthropic `image` blocks; text parts and any other blocks pass
|
|
2803
|
+
// through unchanged.
|
|
2804
|
+
let content = m.content;
|
|
2805
|
+
if (Array.isArray(content)) {
|
|
2806
|
+
content = content.map((b) =>
|
|
2807
|
+
b?.type === "image_url" ? imageUrlBlockToAnthropic(b) || b : b,
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
out.push({ role: "user", content });
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
flush();
|
|
2814
|
+
return out;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/** Map a Claude-Code-style intensity to an Anthropic effort level. */
|
|
2818
|
+
function _intensityToEffort(want) {
|
|
2819
|
+
switch (String(want)) {
|
|
2820
|
+
case "ultra":
|
|
2821
|
+
case "ultrathink":
|
|
2822
|
+
return "xhigh";
|
|
2823
|
+
case "hard":
|
|
2824
|
+
case "think-hard":
|
|
2825
|
+
case "harder":
|
|
2826
|
+
return "high";
|
|
2827
|
+
case "think":
|
|
2828
|
+
return "medium";
|
|
2829
|
+
default:
|
|
2830
|
+
return "high"; // bare `true` → a sensible default for intelligence work
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
/**
|
|
2835
|
+
* Decide the Anthropic `thinking` request params for a model. Returns null
|
|
2836
|
+
* (off) unless the caller opts in via options.thinking (true | "think" |
|
|
2837
|
+
* "hard" | "ultra"). Model-aware per the Claude API:
|
|
2838
|
+
* - Opus 4.6/4.7/4.8, Sonnet 4.6 → adaptive thinking + output_config.effort
|
|
2839
|
+
* - Sonnet 4.5 / Opus 4.0-4.5 / older → legacy enabled+budget (< max_tokens)
|
|
2840
|
+
* - anything else (e.g. Haiku) → null (no thinking)
|
|
2841
|
+
* temperature is never added (it 400s on Opus 4.7/4.8). Note: `xhigh`/`max`
|
|
2842
|
+
* effort are Opus-tier — on Sonnet they may error; left to the caller's intent.
|
|
2843
|
+
* RUNTIME-UNVERIFIED — no Anthropic key to validate the wire shape live.
|
|
2844
|
+
* Exported for tests.
|
|
2845
|
+
*/
|
|
2846
|
+
export function _anthropicThinkingParams(model, options = {}, maxTokens = 8192) {
|
|
2847
|
+
const want = options?.thinking;
|
|
2848
|
+
if (!want) return null; // off by default → request unchanged
|
|
2849
|
+
const m = String(model || "").toLowerCase();
|
|
2850
|
+
const adaptive = /opus-4-(6|7|8)/.test(m) || /sonnet-4-6/.test(m);
|
|
2851
|
+
const legacy =
|
|
2852
|
+
/sonnet-4-5/.test(m) ||
|
|
2853
|
+
/opus-4-(0|1|5)/.test(m) ||
|
|
2854
|
+
/sonnet-4-0/.test(m) ||
|
|
2855
|
+
/sonnet-3/.test(m) ||
|
|
2856
|
+
/opus-3/.test(m);
|
|
2857
|
+
if (adaptive) {
|
|
2858
|
+
const params = { thinking: { type: "adaptive" } };
|
|
2859
|
+
const effort =
|
|
2860
|
+
typeof options.thinkingEffort === "string"
|
|
2861
|
+
? options.thinkingEffort
|
|
2862
|
+
: _intensityToEffort(want);
|
|
2863
|
+
if (effort) params.output_config = { effort };
|
|
2864
|
+
return params;
|
|
2865
|
+
}
|
|
2866
|
+
if (legacy) {
|
|
2867
|
+
let budget = Number(options.thinkingBudget) || 8000;
|
|
2868
|
+
// budget_tokens must be strictly < max_tokens (min 1024) on legacy models
|
|
2869
|
+
if (budget >= maxTokens) budget = Math.max(1024, Math.floor(maxTokens / 2));
|
|
2870
|
+
return { thinking: { type: "enabled", budget_tokens: budget } };
|
|
2871
|
+
}
|
|
2872
|
+
return null; // unknown / Haiku → no thinking
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
export function _normalizeAnthropicResponse(data) {
|
|
2191
2876
|
const content = data.content || [];
|
|
2192
2877
|
const textBlocks = content.filter((b) => b.type === "text");
|
|
2193
2878
|
const toolBlocks = content.filter((b) => b.type === "tool_use");
|
|
2879
|
+
const thinkingBlocks = content.filter(
|
|
2880
|
+
(b) => b.type === "thinking" || b.type === "redacted_thinking",
|
|
2881
|
+
);
|
|
2194
2882
|
|
|
2195
2883
|
const message = {
|
|
2196
2884
|
role: "assistant",
|
|
@@ -2208,6 +2896,13 @@ function _normalizeAnthropicResponse(data) {
|
|
|
2208
2896
|
}));
|
|
2209
2897
|
}
|
|
2210
2898
|
|
|
2899
|
+
// Preserve thinking blocks VERBATIM (incl. signature) so the agent loop can
|
|
2900
|
+
// replay them on the next tool turn — required when extended thinking is on,
|
|
2901
|
+
// harmless (absent) otherwise. _toAnthropicMessages re-emits them first.
|
|
2902
|
+
if (thinkingBlocks.length > 0) {
|
|
2903
|
+
message._thinkingBlocks = thinkingBlocks;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2211
2906
|
return { message };
|
|
2212
2907
|
}
|
|
2213
2908
|
|
|
@@ -2322,12 +3017,23 @@ export async function* agentLoop(messages, options) {
|
|
|
2322
3017
|
externalToolDescriptors: options.externalToolDescriptors || null,
|
|
2323
3018
|
externalToolExecutors: options.externalToolExecutors || null,
|
|
2324
3019
|
mcpClient: options.mcpClient || null,
|
|
3020
|
+
// Parent LLM config — forwarded to spawn_sub_agent so a delegated subagent
|
|
3021
|
+
// inherits the provider/key and can override just the model (cc agents `model:`).
|
|
3022
|
+
llmOptions: {
|
|
3023
|
+
provider: options.provider || null,
|
|
3024
|
+
model: options.model || null,
|
|
3025
|
+
baseUrl: options.baseUrl || null,
|
|
3026
|
+
apiKey: options.apiKey || null,
|
|
3027
|
+
},
|
|
2325
3028
|
parentMessages: messages, // pass parent messages for sub-agent auto-condensation
|
|
2326
3029
|
interaction: options.interaction || null,
|
|
2327
3030
|
shellPolicyOverrides: options.shellPolicyOverrides || null,
|
|
2328
3031
|
approvalGate: options.approvalGate || null,
|
|
2329
3032
|
shellConfirm: options.shellConfirm || null,
|
|
2330
3033
|
additionalDirectories: options.additionalDirectories || null,
|
|
3034
|
+
permissionRules: options.permissionRules || null,
|
|
3035
|
+
permissionConfirm: options.permissionConfirm || null,
|
|
3036
|
+
settingsHooks: options.settingsHooks || null,
|
|
2331
3037
|
autoCheckpoint: options.autoCheckpoint || false,
|
|
2332
3038
|
checkpointSession:
|
|
2333
3039
|
options.checkpointSession || options.sessionId || "agent",
|
|
@@ -2401,6 +3107,10 @@ export async function* agentLoop(messages, options) {
|
|
|
2401
3107
|
sessionId: options.sessionId || null,
|
|
2402
3108
|
};
|
|
2403
3109
|
|
|
3110
|
+
// True once a Stop hook has forced a continuation — passed to the next Stop
|
|
3111
|
+
// hook as `stop_hook_active` so a well-behaved hook won't block forever.
|
|
3112
|
+
let stopHookActive = false;
|
|
3113
|
+
|
|
2404
3114
|
while (budget.hasRemaining()) {
|
|
2405
3115
|
budget.consume();
|
|
2406
3116
|
throwIfAborted(signal);
|
|
@@ -2445,10 +3155,37 @@ export async function* agentLoop(messages, options) {
|
|
|
2445
3155
|
try {
|
|
2446
3156
|
const compactor = await _getAutoCompactor(options);
|
|
2447
3157
|
if (compactor && compactor.shouldAutoCompact(messages)) {
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
3158
|
+
// settings.json PreCompact hooks: a `block` decision SKIPS this
|
|
3159
|
+
// compaction round (e.g. the hook archived / owns the history). Fires
|
|
3160
|
+
// right before the history would be compacted.
|
|
3161
|
+
let preCompactBlocked = false;
|
|
3162
|
+
let preCompactReason = null;
|
|
3163
|
+
if (options.settingsHooks) {
|
|
3164
|
+
try {
|
|
3165
|
+
const pc = runObserveHooks(
|
|
3166
|
+
options.settingsHooks,
|
|
3167
|
+
"PreCompact",
|
|
3168
|
+
{
|
|
3169
|
+
trigger: "auto",
|
|
3170
|
+
message_count: messages.length,
|
|
3171
|
+
session_id: options.sessionId || null,
|
|
3172
|
+
},
|
|
3173
|
+
{ cwd: options.cwd || process.cwd() },
|
|
3174
|
+
);
|
|
3175
|
+
if (pc && pc.decision === "block") {
|
|
3176
|
+
preCompactBlocked = true;
|
|
3177
|
+
preCompactReason = pc.reason || null;
|
|
3178
|
+
}
|
|
3179
|
+
} catch (_err) {
|
|
3180
|
+
// observe-only
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
if (preCompactBlocked) {
|
|
3184
|
+
yield { type: "compaction-skipped", runId, reason: preCompactReason };
|
|
3185
|
+
}
|
|
3186
|
+
const { messages: compacted, stats } = preCompactBlocked
|
|
3187
|
+
? { messages, stats: { saved: 0 } }
|
|
3188
|
+
: await compactor.compress(messages, { preserveToolPairs: true });
|
|
2452
3189
|
if (stats.saved > 0 && compacted.length < messages.length) {
|
|
2453
3190
|
messages.splice(0, messages.length, ...compacted);
|
|
2454
3191
|
// Persist the compaction so a later --resume rebuilds from the
|
|
@@ -2538,6 +3275,43 @@ export async function* agentLoop(messages, options) {
|
|
|
2538
3275
|
|
|
2539
3276
|
if (!toolCalls || toolCalls.length === 0) {
|
|
2540
3277
|
yield { type: "response-complete", content: msg.content || "" };
|
|
3278
|
+
// settings.json Stop hooks: a `block` decision FORCES the agent to keep
|
|
3279
|
+
// going instead of stopping — the reason is injected as a new instruction.
|
|
3280
|
+
// `stop_hook_active` lets the hook avoid an infinite loop; the iteration
|
|
3281
|
+
// budget is the hard backstop.
|
|
3282
|
+
if (options.settingsHooks) {
|
|
3283
|
+
let stopOutcome = null;
|
|
3284
|
+
try {
|
|
3285
|
+
stopOutcome = runObserveHooks(
|
|
3286
|
+
options.settingsHooks,
|
|
3287
|
+
"Stop",
|
|
3288
|
+
{
|
|
3289
|
+
stop_hook_active: stopHookActive,
|
|
3290
|
+
final_response: String(msg.content || "").substring(0, 2000),
|
|
3291
|
+
session_id: options.sessionId || null,
|
|
3292
|
+
},
|
|
3293
|
+
{ cwd: options.cwd || process.cwd() },
|
|
3294
|
+
);
|
|
3295
|
+
} catch (_err) {
|
|
3296
|
+
stopOutcome = null; // never affect the run outcome
|
|
3297
|
+
}
|
|
3298
|
+
if (stopOutcome && stopOutcome.decision === "block") {
|
|
3299
|
+
stopHookActive = true;
|
|
3300
|
+
messages.push({ role: "assistant", content: msg.content || "" });
|
|
3301
|
+
messages.push({
|
|
3302
|
+
role: "user",
|
|
3303
|
+
content:
|
|
3304
|
+
stopOutcome.reason ||
|
|
3305
|
+
"A Stop hook requested that you keep working.",
|
|
3306
|
+
});
|
|
3307
|
+
yield {
|
|
3308
|
+
type: "stop-hook-continue",
|
|
3309
|
+
runId,
|
|
3310
|
+
reason: stopOutcome.reason || null,
|
|
3311
|
+
};
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
2541
3315
|
yield { type: "run-ended", runId, reason: "complete" };
|
|
2542
3316
|
return;
|
|
2543
3317
|
}
|
|
@@ -2626,7 +3400,11 @@ export function formatToolArgs(name, args) {
|
|
|
2626
3400
|
case "edit_file_hashed":
|
|
2627
3401
|
return `${args.path} @${args.anchor_hash}`;
|
|
2628
3402
|
case "run_shell":
|
|
2629
|
-
return args.command;
|
|
3403
|
+
return args.run_in_background ? `${args.command} (background)` : args.command;
|
|
3404
|
+
case "check_shell":
|
|
3405
|
+
return args.task_id
|
|
3406
|
+
? `${args.task_id}${args.kill ? " (kill)" : ""}`
|
|
3407
|
+
: "list";
|
|
2630
3408
|
case "git":
|
|
2631
3409
|
return args.command;
|
|
2632
3410
|
case "search_files":
|