chainlesschain 0.162.31 → 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-BqWP6FKu.js → AIOps-Cg_uWAVl.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CXwMgOvX.js → ActionButton-DSFtQ1c2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DAebZ4IY.js → Analytics-BMxpkw8y.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CYsqYoME.js → AppLayout-tgVxlmsx.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-BbTtX1Nf.js → Audit-DwzGllcp.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DgqY2Eb-.js → Backup-BG28Y2MV.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Cq2ZuSoO.js → BaseInput-TXthbazl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-D2kqpUyO.js → Chat-D096SxaD.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-PIx0Eu9I.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-_9swHpyc.js → Checkbox-Czttw1JS.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-Cr9YbCPl.js → Codegen-DZtMgv4q.js} +1 -1
- package/src/assets/web-panel/assets/{Col--wdpCMxx.js → Col-D3DnfExY.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DuFcVnLu.js → Community-Bj5AdwqY.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-1yzYeT04.js → Compact-BQ8Zszub.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dq3aU9Df.js → Compliance-DXacb34n.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CrWcnIg8.js → Cowork-BgMUBTkw.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Bh6fKZ0h.js → Cron-fqBWOqlN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-8ofPaWVW.js → Crosschain-E4oa1MWy.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D3EiYm3w.js → DID-pwgfYZaV.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BFjEdFne.js → Dashboard-n8mdLFIR.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-pYVPcP6O.js → Dropdown--6DYqxk7.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-zBPodwJ1.js → EmailListRenderer-CkjQluz3.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CyQTW6PW.js → FamilyGuardDashboard-u-QTQ-OC.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Ctaq3zYq.js → Federation-D219M5Qc.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CWYJCLq1.js → FormItemContext-BBU_aopC.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B1g6t9R9.js → GenericCardRenderer-pTMCIHcM.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DH-v8iwd.js → Git-ClcCARWt.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-jZxXvOs5.js → Governance-CvUi3I93.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-D07LRghn.js → Inference-DT-a4pVg.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DnGtRZhx.js → KnowledgeGraph-DHMs2LY8.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-D2pM9C4W.js → Logs-D2s4eV1N.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-UyIO7C7r.js → Marketplace-YC5-fx-6.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Bf1gvZPf.js → McpTools-7JHTEC4T.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-C1bWj4RN.js → Memory-BudotVLD.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-C_Ot1H_a.js → MobileBridge-CAiRyLVU.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-zr-PpsT_.js → MobileProjects-CrJJOCFw.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CnzFUz5J.js → Mtc-d0iY0CeK.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CAAh99wz.js → MtcAudit-aI2cG1UP.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-D6IAg6HE.js → Multisig-4bF70khG.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BFMarxb0.js → NLProgramming-CwLib1S7.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BRp9ro3t.js → Notes-Wt7AuFRU.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C0Au3Cxb.js → NotificationSettings-D081vV_7.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-ISp6btRY.js → OrderTableRenderer-DCPei1L9.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DYoxLBRX.js → Organization-BNEsUNdP.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-rO8JJWGJ.js → Overflow-B_1iUXDD.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DJleeXIK.js → P2P-Dbc-kNwJ.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DM5qghFp.js → PdhVaultBrowser-D8Xh289k.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-D5v4Beya.js → Permissions-C77mM6-n.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-c2ZTX0Pv.js → PersonalDataHub-Dj0J3r_K.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-Crrkyhpz.js → Pipeline-B6F0WQ2C.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DZVyrJKa.js → Privacy-eDKOkyyq.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DKg7J0gz.js → ProjectInit-DAWwhr5_.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-3ndmTvVH.js → ProjectSettings-DwdK8k6I.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Cb3p5QAP.js +1 -0
- package/src/assets/web-panel/assets/{Providers-BeqBVMhB.js → Providers--DcYxQfN.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DKAAxzuA.js → QuickAsk-DU268niT.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Byu7IGei.js → Recommend-ChnflhV1.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-BKhWAmCu.js → Reputation-DSsY3bQG.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BFtn11O6.js → Row-Zb-EjmgQ.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-D5a0PT0k.js → RssFeed-CGLiixZB.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DAkuaZNe.js → Search-Dhr_po-U.js} +1 -1
- package/src/assets/web-panel/assets/{Security-C79Ml2Ms.js → Security-GMYNhGsR.js} +2 -2
- package/src/assets/web-panel/assets/{Services-BBk_jH6-.js → Services-DiOpnVY0.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Cy0VvL0M.js → Skeleton-DG3ez6ME.js} +1 -1
- package/src/assets/web-panel/assets/Skills-DZGptytP.js +1 -0
- package/src/assets/web-panel/assets/{Sla-CbX1f8xN.js → Sla-CtGpE3xA.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BIkoUjws.js → SpeechSettings-DQFw6Cf9.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DG6Swk7G.js → SyncSettings-C8X78RpX.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-C9R8sgyi.js → Tasks-DtVkhWCV.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-AaJPeCIz.js → Templates-SF9_ZWsV.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-jVFRofww.js → Tenant-BbIQSVZz.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DHBMzfK6.js → Terminal-DKr5zDwu.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-9RFfOHSI.js → TimelineRenderer-BtLaNaWr.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-ZTfwuABF.js → Tokens-CfYbk2NG.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Xo7uZNQs.js → Trigger-BLX_XDP0.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-C0cTPYvn.js → Trust-BWxUv9PR.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DmMKio71.js → UkeySign-DRwTyQD4.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DP7B-oGT.js → VideoEditing-BsC4VOSo.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B1kZDARo.js → Wallet-CSsO1NJU.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Bo5kBx27.js → WebAuthn-z1MxiFzS.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DGI9SNHH.js → WorkflowEditor-B1vV7uuJ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-y97W1CIG.js → chat-C0NJRaL2.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DtTNo0sH.js → colors-CHRiteWF.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-D0q0exuS.js → compact-item-2XmBBKPD.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D7pLFs2I.js → createContext-DkedHC38.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DmNpkOdC.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CXjG5B2j.js → hasIn-Bpn9Xrlw.js} +1 -1
- package/src/assets/web-panel/assets/index-7nAysteg.js +1 -0
- package/src/assets/web-panel/assets/{index-B3y_4OdG.js → index-B5NGWgHp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJUf19Wd.js → index-BItcSqan.js} +3 -3
- package/src/assets/web-panel/assets/index-BKWSQilQ.js +1 -0
- package/src/assets/web-panel/assets/{index-POaFzYGS.js → index-BN068mCR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSdhC7Qo.js → index-BOsIgPge.js} +1 -1
- package/src/assets/web-panel/assets/{index-4mWZhCzz.js → index-BYUd69vM.js} +1 -1
- package/src/assets/web-panel/assets/{index-_3wPBMKt.js → index-BYmwEaIk.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4dPdrvC.js → index-BZ1gOoiG.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_hjkMtX.js → index-BfY9U3X5.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dr45Nm9V.js → index-BveL_4n3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgmvrPJH.js → index-CCg6ZY4t.js} +1 -1
- package/src/assets/web-panel/assets/{index-gFLQe31v.js → index-CJOoo72F.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY6KLlgG.js → index-CToQxpWz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkGFqlYX.js → index-CWgWrrWs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BO644Q4S.js → index-CdR7RfRP.js} +1 -1
- package/src/assets/web-panel/assets/{index-kvV0f4tV.js → index-Cljnfuxu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BU944DeT.js → index-CxvA72CP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKrbutAQ.js → index-CyJpmSHZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-qoB3whR9.js → index-D7U411hK.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cbqu804A.js → index-D9mNfpxi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjCawXk1.js → index-DAFLFMXQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdhEYW2a.js → index-DAeHmElB.js} +1 -1
- package/src/assets/web-panel/assets/{index-EaIfumgW.js → index-DDy_RDjs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzCPx1cq.js → index-DE5Qm9UI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgyrM0UN.js → index-DM9JrnYi.js} +1 -1
- package/src/assets/web-panel/assets/{index-8jxbZupG.js → index-DMbF-Euw.js} +1 -1
- package/src/assets/web-panel/assets/{index-1dwtkcJv.js → index-DUBsq_1G.js} +1 -1
- package/src/assets/web-panel/assets/{index-TrBGgrwG.js → index-De49R7TX.js} +1 -1
- package/src/assets/web-panel/assets/{index-aarO4HT9.js → index-De5vOO9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnLrbXDA.js → index-Dk7P-q3n.js} +1 -1
- package/src/assets/web-panel/assets/{index-YWOEx3rP.js → index-DryKGM_t.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPXhU-jp.js → index-DtU4qZRF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFsPe2N7.js → index-NuBsCRaR.js} +1 -1
- package/src/assets/web-panel/assets/{index-6np5ESBM.js → index-Sk3-3tKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-bVJvqDAz.js → index-alGjpoM1.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ct6xtKkc.js → index-cfSUlOfY.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_4WcI1V.js → index-i4W_EAuh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqVjUN8b.js → index-uHGxyZtQ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BnXISaAa.js → initDefaultProps-DlDE-QgI.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ChY7C0zJ.js → motion-CodUbIRF.js} +1 -1
- package/src/assets/web-panel/assets/{move-ByFZMFM5.js → move-DaLwsHeR.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BYeliY1H.js → omit-DdVg-3rL.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B9dcAKnu.js → pickAttrs-KLR1EVCo.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-D3F_txz7.js → placementArrow-ChV7HvNw.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-ClkwY7wS.js → responsiveObserve-BB_A8dBt.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BNgy2Eea.js → slide-Bc1tQnIK.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Bv3heMCD.js → statusUtils-CgrveSb0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DVdlHbQm.js → styleChecker-vXAYhhjz.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-alrRY5BK.js → useFlexGapSupport-BCIMPfq9.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CcVh0-Vu.js → useFs-DMZGdr6G.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CkkHPhyq.js → usePersonalDataHub-118tWI_Z.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DWi0X9WN.js → vnode-Z7O2Y7JP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DCbqxxLH.js → zoom-BXym6zmD.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +27 -0
- package/src/commands/checkpoint.js +253 -53
- package/src/commands/compact.js +150 -0
- package/src/commands/goal.js +417 -0
- package/src/commands/hub.js +7 -0
- package/src/harness/prompt-compressor.js +71 -1
- package/src/index.js +4 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/checkpoint-store.js +523 -0
- package/src/lib/goal-context.js +87 -0
- package/src/lib/goal-store.js +308 -0
- package/src/repl/agent-repl.js +43 -7
- package/src/runtime/agent-core.js +245 -1
- package/src/runtime/headless-runner.js +25 -0
- package/src/runtime/headless-stream.js +13 -0
- package/src/runtime/policies/agent-policy.js +1 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-C-svYkrC.js +0 -1
- package/src/assets/web-panel/assets/Projects-ll5wnj2L.js +0 -1
- package/src/assets/web-panel/assets/Skills-OQNky3uI.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BDK34w0I.js +0 -1
- package/src/assets/web-panel/assets/index-B6SaRuCI.js +0 -1
- package/src/assets/web-panel/assets/index-B9ekWb3I.js +0 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-store — cross-session persistent goals / OKRs for `cc goal`.
|
|
3
|
+
*
|
|
4
|
+
* Unlike a session (short-lived context) or a checkpoint (file state), a goal
|
|
5
|
+
* is a long-lived objective the agent should advance toward across many
|
|
6
|
+
* sessions. This is the standalone store + ops; wiring the goal into the agent
|
|
7
|
+
* loop (so each turn is measured against it) lives in goal-context.js.
|
|
8
|
+
*
|
|
9
|
+
* On-disk layout (under <home>/goals, overridable via opts.root for tests):
|
|
10
|
+
* <root>/<id>.json one goal per file
|
|
11
|
+
*
|
|
12
|
+
* Distinct from:
|
|
13
|
+
* - cc session (short-term conversation context)
|
|
14
|
+
* - cc memory (durable facts, not objectives)
|
|
15
|
+
* - cc planmode (a single run's plan, not a cross-session objective)
|
|
16
|
+
* - cc workflow (execution orchestration, not intent)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { getHomeDir } from "./paths.js";
|
|
22
|
+
|
|
23
|
+
/** Valid goal lifecycle states. `active` goals are the ones injected. */
|
|
24
|
+
export const GOAL_STATUS = Object.freeze({
|
|
25
|
+
ACTIVE: "active",
|
|
26
|
+
PAUSED: "paused",
|
|
27
|
+
DONE: "done",
|
|
28
|
+
ABANDONED: "abandoned",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const STATUS_VALUES = new Set(Object.values(GOAL_STATUS));
|
|
32
|
+
|
|
33
|
+
function defaultRoot() {
|
|
34
|
+
return path.join(getHomeDir(), "goals");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureDir(dir) {
|
|
38
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function newId(prefix) {
|
|
42
|
+
// Date.now/random are fine here (plain CLI lib, not a resumable workflow).
|
|
43
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
44
|
+
return `${prefix}-${Date.now()}-${rand}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function nowIso() {
|
|
48
|
+
return new Date().toISOString();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function goalFile(root, id) {
|
|
52
|
+
return path.join(root, `${id}.json`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Clamp a value to a 0–100 integer percentage, or null when not a number. */
|
|
56
|
+
function clampPct(v) {
|
|
57
|
+
if (v == null || v === "") return null;
|
|
58
|
+
const n = Math.round(Number(v));
|
|
59
|
+
if (!Number.isFinite(n)) return null;
|
|
60
|
+
return Math.max(0, Math.min(100, n));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Derive a 0–100 progress from completed key results. Returns null when there
|
|
65
|
+
* are no key results (caller may keep a manually-set progress instead).
|
|
66
|
+
*/
|
|
67
|
+
function derivedProgress(keyResults) {
|
|
68
|
+
if (!Array.isArray(keyResults) || keyResults.length === 0) return null;
|
|
69
|
+
const done = keyResults.filter((k) => k.done).length;
|
|
70
|
+
return Math.round((done / keyResults.length) * 100);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a goal.
|
|
75
|
+
* @param {object} input { objective, title, keyResults?: string[]|object[] }
|
|
76
|
+
* @param {object} [opts] { root }
|
|
77
|
+
* @returns {object} the persisted goal
|
|
78
|
+
*/
|
|
79
|
+
export function createGoal(input = {}, opts = {}) {
|
|
80
|
+
const root = opts.root || defaultRoot();
|
|
81
|
+
const objective = String(input.objective || "").trim();
|
|
82
|
+
if (!objective) {
|
|
83
|
+
throw new Error("createGoal requires an objective");
|
|
84
|
+
}
|
|
85
|
+
const id = input.id || newId("goal");
|
|
86
|
+
const keyResults = (input.keyResults || []).map((kr) => normalizeKr(kr));
|
|
87
|
+
const goal = {
|
|
88
|
+
id,
|
|
89
|
+
title: String(input.title || objective).trim(),
|
|
90
|
+
objective,
|
|
91
|
+
keyResults,
|
|
92
|
+
status: GOAL_STATUS.ACTIVE,
|
|
93
|
+
progress: derivedProgress(keyResults) ?? 0,
|
|
94
|
+
linkedSessions: [],
|
|
95
|
+
notes: [],
|
|
96
|
+
drift: { lastProgressAt: null, flags: [] },
|
|
97
|
+
createdAt: nowIso(),
|
|
98
|
+
updatedAt: nowIso(),
|
|
99
|
+
};
|
|
100
|
+
ensureDir(root);
|
|
101
|
+
fs.writeFileSync(goalFile(root, id), JSON.stringify(goal, null, 2), "utf-8");
|
|
102
|
+
return goal;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeKr(kr) {
|
|
106
|
+
if (typeof kr === "string") {
|
|
107
|
+
return {
|
|
108
|
+
id: newId("kr"),
|
|
109
|
+
text: kr.trim(),
|
|
110
|
+
target: null,
|
|
111
|
+
current: 0,
|
|
112
|
+
done: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
id: kr.id || newId("kr"),
|
|
117
|
+
text: String(kr.text || "").trim(),
|
|
118
|
+
target: kr.target == null ? null : Number(kr.target),
|
|
119
|
+
current: Number(kr.current) || 0,
|
|
120
|
+
done: !!kr.done,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Load a goal by id, or null. */
|
|
125
|
+
export function getGoal(id, opts = {}) {
|
|
126
|
+
const root = opts.root || defaultRoot();
|
|
127
|
+
const file = goalFile(root, id);
|
|
128
|
+
if (!fs.existsSync(file)) return null;
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function saveGoal(goal, opts = {}) {
|
|
137
|
+
const root = opts.root || defaultRoot();
|
|
138
|
+
ensureDir(root);
|
|
139
|
+
goal.updatedAt = nowIso();
|
|
140
|
+
fs.writeFileSync(
|
|
141
|
+
goalFile(root, goal.id),
|
|
142
|
+
JSON.stringify(goal, null, 2),
|
|
143
|
+
"utf-8",
|
|
144
|
+
);
|
|
145
|
+
return goal;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** List goals, newest first. Optionally filter by status. */
|
|
149
|
+
export function listGoals(opts = {}) {
|
|
150
|
+
const root = opts.root || defaultRoot();
|
|
151
|
+
if (!fs.existsSync(root)) return [];
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const name of fs.readdirSync(root)) {
|
|
154
|
+
if (!name.endsWith(".json")) continue;
|
|
155
|
+
const g = getGoal(name.slice(0, -5), { root });
|
|
156
|
+
if (!g) continue;
|
|
157
|
+
if (opts.status && g.status !== opts.status) continue;
|
|
158
|
+
out.push(g);
|
|
159
|
+
}
|
|
160
|
+
return out.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Mutate a goal via `fn(goal)`; throws if not found. */
|
|
164
|
+
function mutate(id, fn, opts = {}) {
|
|
165
|
+
const goal = getGoal(id, opts);
|
|
166
|
+
if (!goal) throw new Error(`no such goal: ${id}`);
|
|
167
|
+
fn(goal);
|
|
168
|
+
return saveGoal(goal, opts);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Add a key result to a goal. */
|
|
172
|
+
export function addKeyResult(id, text, krOpts = {}, opts = {}) {
|
|
173
|
+
return mutate(
|
|
174
|
+
id,
|
|
175
|
+
(g) => {
|
|
176
|
+
g.keyResults.push(normalizeKr({ text, target: krOpts.target }));
|
|
177
|
+
const dp = derivedProgress(g.keyResults);
|
|
178
|
+
if (dp != null) g.progress = dp;
|
|
179
|
+
},
|
|
180
|
+
opts,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Update a key result (current value and/or done flag). */
|
|
185
|
+
export function setKeyResult(id, krId, patch = {}, opts = {}) {
|
|
186
|
+
return mutate(
|
|
187
|
+
id,
|
|
188
|
+
(g) => {
|
|
189
|
+
const kr = g.keyResults.find((k) => k.id === krId);
|
|
190
|
+
if (!kr) throw new Error(`no such key result: ${krId}`);
|
|
191
|
+
if (patch.current != null) kr.current = Number(patch.current);
|
|
192
|
+
if (patch.done != null) kr.done = !!patch.done;
|
|
193
|
+
if (
|
|
194
|
+
kr.target != null &&
|
|
195
|
+
patch.current != null &&
|
|
196
|
+
Number(patch.current) >= kr.target
|
|
197
|
+
) {
|
|
198
|
+
kr.done = true;
|
|
199
|
+
}
|
|
200
|
+
const dp = derivedProgress(g.keyResults);
|
|
201
|
+
if (dp != null) g.progress = dp;
|
|
202
|
+
g.drift.lastProgressAt = nowIso();
|
|
203
|
+
},
|
|
204
|
+
opts,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Record progress: set an explicit percentage and/or append a note.
|
|
210
|
+
* @param {object} input { pct?, note?, by? }
|
|
211
|
+
*/
|
|
212
|
+
export function recordProgress(id, input = {}, opts = {}) {
|
|
213
|
+
return mutate(
|
|
214
|
+
id,
|
|
215
|
+
(g) => {
|
|
216
|
+
const pct = clampPct(input.pct);
|
|
217
|
+
if (pct != null) g.progress = pct;
|
|
218
|
+
if (input.note) {
|
|
219
|
+
g.notes.push({
|
|
220
|
+
at: nowIso(),
|
|
221
|
+
text: String(input.note),
|
|
222
|
+
by: input.by === "agent" ? "agent" : "user",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
g.drift.lastProgressAt = nowIso();
|
|
226
|
+
},
|
|
227
|
+
opts,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Attach a session id to a goal (idempotent). */
|
|
232
|
+
export function linkSession(id, sessionId, opts = {}) {
|
|
233
|
+
if (!sessionId) throw new Error("linkSession requires a sessionId");
|
|
234
|
+
return mutate(
|
|
235
|
+
id,
|
|
236
|
+
(g) => {
|
|
237
|
+
if (!g.linkedSessions.includes(sessionId)) {
|
|
238
|
+
g.linkedSessions.push(sessionId);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
opts,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Detach a session id from a goal. */
|
|
246
|
+
export function unlinkSession(id, sessionId, opts = {}) {
|
|
247
|
+
return mutate(
|
|
248
|
+
id,
|
|
249
|
+
(g) => {
|
|
250
|
+
g.linkedSessions = g.linkedSessions.filter((s) => s !== sessionId);
|
|
251
|
+
},
|
|
252
|
+
opts,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Set a goal's status (active/paused/done/abandoned). */
|
|
257
|
+
export function setStatus(id, status, opts = {}) {
|
|
258
|
+
if (!STATUS_VALUES.has(status)) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`invalid status "${status}" — expected one of: ${[...STATUS_VALUES].join(", ")}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return mutate(
|
|
264
|
+
id,
|
|
265
|
+
(g) => {
|
|
266
|
+
g.status = status;
|
|
267
|
+
if (status === GOAL_STATUS.DONE && g.progress < 100) g.progress = 100;
|
|
268
|
+
},
|
|
269
|
+
opts,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Delete a goal. Returns true if it existed. */
|
|
274
|
+
export function deleteGoal(id, opts = {}) {
|
|
275
|
+
const root = opts.root || defaultRoot();
|
|
276
|
+
const file = goalFile(root, id);
|
|
277
|
+
if (!fs.existsSync(file)) return false;
|
|
278
|
+
fs.rmSync(file);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Resolve the goal that should be bound to the current run, in priority order:
|
|
284
|
+
* 1. explicit id (--goal <id>)
|
|
285
|
+
* 2. an active goal linked to the current session
|
|
286
|
+
* 3. when exactly one active goal exists, that one
|
|
287
|
+
* 4. null
|
|
288
|
+
*
|
|
289
|
+
* Only `active` goals are ever auto-resolved (steps 2–3); an explicit id is
|
|
290
|
+
* honored regardless of status so a user can re-inspect a paused goal.
|
|
291
|
+
*
|
|
292
|
+
* @param {object} [sel] { explicitId, sessionId }
|
|
293
|
+
* @param {object} [opts] { root }
|
|
294
|
+
*/
|
|
295
|
+
export function resolveActiveGoal(sel = {}, opts = {}) {
|
|
296
|
+
if (sel.explicitId) {
|
|
297
|
+
return getGoal(sel.explicitId, opts);
|
|
298
|
+
}
|
|
299
|
+
const active = listGoals({ ...opts, status: GOAL_STATUS.ACTIVE });
|
|
300
|
+
if (active.length === 0) return null;
|
|
301
|
+
if (sel.sessionId) {
|
|
302
|
+
const linked = active.find((g) => g.linkedSessions.includes(sel.sessionId));
|
|
303
|
+
if (linked) return linked;
|
|
304
|
+
}
|
|
305
|
+
if (active.length === 1) return active[0];
|
|
306
|
+
// Ambiguous (multiple active, none linked) — caller must pick explicitly.
|
|
307
|
+
return null;
|
|
308
|
+
}
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -92,8 +92,18 @@ async function executeTool(name, args) {
|
|
|
92
92
|
*/
|
|
93
93
|
async function agentLoop(messages, options) {
|
|
94
94
|
const usageEvents = [];
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// The REPL runs its own auto-compaction (after each turn, with metrics +
|
|
96
|
+
// persisted compact events), so opt out of the agent loop's in-loop
|
|
97
|
+
// compaction to avoid compacting the same history twice.
|
|
98
|
+
for await (const event of coreAgentLoop(messages, {
|
|
99
|
+
autoCompact: false,
|
|
100
|
+
...options,
|
|
101
|
+
})) {
|
|
102
|
+
if (event.type === "checkpoint") {
|
|
103
|
+
process.stdout.write(
|
|
104
|
+
chalk.gray(` ⎌ checkpoint ${event.id} (before ${event.tool})\n`),
|
|
105
|
+
);
|
|
106
|
+
} else if (event.type === "tool-executing") {
|
|
97
107
|
process.stdout.write(
|
|
98
108
|
chalk.gray(
|
|
99
109
|
` [${event.tool}] ${formatToolArgs(event.tool, event.args)}\n`,
|
|
@@ -158,6 +168,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
158
168
|
const additionalDirectories = Array.isArray(options.additionalDirectories)
|
|
159
169
|
? options.additionalDirectories
|
|
160
170
|
: [];
|
|
171
|
+
// Snapshot the work tree before each mutating tool (git engine) so the user
|
|
172
|
+
// can `cc checkpoint restore` to just before any tool call.
|
|
173
|
+
const autoCheckpoint = options.autoCheckpoint === true;
|
|
161
174
|
|
|
162
175
|
// --fallback-model: retry a turn's LLM call once on a backup model when the
|
|
163
176
|
// primary errors out (overload / network). Built once; passed into every
|
|
@@ -718,8 +731,10 @@ export async function startAgentRepl(options = {}) {
|
|
|
718
731
|
|
|
719
732
|
if (trimmed === "/compact") {
|
|
720
733
|
if (_compressor && messages.length > 3) {
|
|
721
|
-
const { messages: compacted, stats } =
|
|
722
|
-
|
|
734
|
+
const { messages: compacted, stats } = await _compressor.compress(
|
|
735
|
+
messages,
|
|
736
|
+
{ preserveToolPairs: true },
|
|
737
|
+
);
|
|
723
738
|
messages.length = 0;
|
|
724
739
|
messages.push(...compacted);
|
|
725
740
|
recordCompressionMetric(stats, {
|
|
@@ -1521,6 +1536,23 @@ export async function startAgentRepl(options = {}) {
|
|
|
1521
1536
|
try {
|
|
1522
1537
|
process.stdout.write("\n");
|
|
1523
1538
|
const iterationBudget = new IterationBudget({ owner: sessionId });
|
|
1539
|
+
// Bind a cross-session goal (cc goal) into this run, if one resolves.
|
|
1540
|
+
// Composes WITH defaultPrepareCall — never replaces it. Best-effort.
|
|
1541
|
+
let prepareCall = defaultPrepareCall;
|
|
1542
|
+
try {
|
|
1543
|
+
const { resolveActiveGoal } = await import("../lib/goal-store.js");
|
|
1544
|
+
const boundGoal = resolveActiveGoal({ sessionId });
|
|
1545
|
+
if (boundGoal) {
|
|
1546
|
+
const { goalPrepareCall, composePrepareCall } =
|
|
1547
|
+
await import("../lib/goal-context.js");
|
|
1548
|
+
prepareCall = composePrepareCall([
|
|
1549
|
+
defaultPrepareCall,
|
|
1550
|
+
goalPrepareCall(boundGoal),
|
|
1551
|
+
]);
|
|
1552
|
+
}
|
|
1553
|
+
} catch (_e) {
|
|
1554
|
+
/* goal binding is best-effort — fall back to defaultPrepareCall */
|
|
1555
|
+
}
|
|
1524
1556
|
const { content: response, usageEvents } = await agentLoop(messages, {
|
|
1525
1557
|
provider,
|
|
1526
1558
|
model: activeModel,
|
|
@@ -1531,7 +1563,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
1531
1563
|
sessionId,
|
|
1532
1564
|
cwd: process.cwd(),
|
|
1533
1565
|
additionalDirectories,
|
|
1534
|
-
|
|
1566
|
+
autoCheckpoint,
|
|
1567
|
+
checkpointSession: sessionId,
|
|
1568
|
+
prepareCall,
|
|
1535
1569
|
approvalGate: _approvalGate,
|
|
1536
1570
|
mcpClient: _bundleMcpClient || undefined,
|
|
1537
1571
|
chatFn: _fallbackChatFn,
|
|
@@ -1613,8 +1647,10 @@ export async function startAgentRepl(options = {}) {
|
|
|
1613
1647
|
_compressor.shouldAutoCompact(messages)
|
|
1614
1648
|
) {
|
|
1615
1649
|
try {
|
|
1616
|
-
const { messages: compacted, stats } =
|
|
1617
|
-
|
|
1650
|
+
const { messages: compacted, stats } = await _compressor.compress(
|
|
1651
|
+
messages,
|
|
1652
|
+
{ preserveToolPairs: true },
|
|
1653
|
+
);
|
|
1618
1654
|
messages.length = 0;
|
|
1619
1655
|
messages.push(...compacted);
|
|
1620
1656
|
recordCompressionMetric(stats, {
|
|
@@ -1663,7 +1663,21 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1663
1663
|
throwIfAborted(signal);
|
|
1664
1664
|
|
|
1665
1665
|
if (provider === "ollama") {
|
|
1666
|
-
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, {
|
|
1667
1681
|
method: "POST",
|
|
1668
1682
|
headers: { "Content-Type": "application/json" },
|
|
1669
1683
|
signal,
|
|
@@ -1817,6 +1831,109 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1817
1831
|
return out;
|
|
1818
1832
|
}
|
|
1819
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
|
+
|
|
1820
1937
|
function _normalizeAnthropicResponse(data) {
|
|
1821
1938
|
const content = data.content || [];
|
|
1822
1939
|
const textBlocks = content.filter((b) => b.type === "text");
|
|
@@ -1843,11 +1960,58 @@ function _normalizeAnthropicResponse(data) {
|
|
|
1843
1960
|
|
|
1844
1961
|
// ─── Agent loop (async generator) ─────────────────────────────────────────
|
|
1845
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
|
+
|
|
1846
2009
|
/**
|
|
1847
2010
|
* Async generator that drives the agentic tool-use loop.
|
|
1848
2011
|
*
|
|
1849
2012
|
* Yields events:
|
|
1850
2013
|
* { type: "slot-filling", slot, question } — when asking user for missing info
|
|
2014
|
+
* { type: "checkpoint", id, tool } — auto-checkpoint before a mutating tool
|
|
1851
2015
|
* { type: "tool-executing", tool, args }
|
|
1852
2016
|
* { type: "tool-result", tool, result, error }
|
|
1853
2017
|
* { type: "response-complete", content }
|
|
@@ -1855,6 +2019,38 @@ function _normalizeAnthropicResponse(data) {
|
|
|
1855
2019
|
* @param {Array} messages - mutable messages array (will be appended to)
|
|
1856
2020
|
* @param {object} options - provider, model, baseUrl, apiKey, contextEngine, hookDb, skillLoader, cwd, slotFiller, interaction
|
|
1857
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
|
+
|
|
1858
2054
|
export async function* agentLoop(messages, options) {
|
|
1859
2055
|
// Shared iteration budget — replaces hardcoded MAX_ITERATIONS.
|
|
1860
2056
|
// When options.iterationBudget is provided (e.g. from parent agent),
|
|
@@ -1879,6 +2075,9 @@ export async function* agentLoop(messages, options) {
|
|
|
1879
2075
|
approvalGate: options.approvalGate || null,
|
|
1880
2076
|
shellConfirm: options.shellConfirm || null,
|
|
1881
2077
|
additionalDirectories: options.additionalDirectories || null,
|
|
2078
|
+
autoCheckpoint: options.autoCheckpoint || false,
|
|
2079
|
+
checkpointSession:
|
|
2080
|
+
options.checkpointSession || options.sessionId || "agent",
|
|
1882
2081
|
};
|
|
1883
2082
|
|
|
1884
2083
|
throwIfAborted(signal);
|
|
@@ -1979,6 +2178,42 @@ export async function* agentLoop(messages, options) {
|
|
|
1979
2178
|
};
|
|
1980
2179
|
}
|
|
1981
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
|
+
|
|
1982
2217
|
// Turn-scoped context injection (open-agents prepareCall parity).
|
|
1983
2218
|
// prepareCall runs fresh each iteration and returns an ephemeral
|
|
1984
2219
|
// system-message supplement that is NOT persisted to messages history.
|
|
@@ -2050,6 +2285,15 @@ export async function* agentLoop(messages, options) {
|
|
|
2050
2285
|
toolArgs = {};
|
|
2051
2286
|
}
|
|
2052
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
|
+
|
|
2053
2297
|
yield { type: "tool-executing", tool: toolName, args: toolArgs };
|
|
2054
2298
|
|
|
2055
2299
|
let toolResult;
|