@kolisachint/hoocode-agent 0.4.130 → 0.4.132
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/core/context-gc.d.ts +14 -2
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +73 -22
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +24 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/extensions/core/ask-options.d.ts.map +1 -1
- package/dist/extensions/core/ask-options.js +42 -0
- package/dist/extensions/core/ask-options.js.map +1 -1
- package/dist/extensions/core/loop.d.ts +12 -0
- package/dist/extensions/core/loop.d.ts.map +1 -1
- package/dist/extensions/core/loop.js +35 -5
- package/dist/extensions/core/loop.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -20,13 +20,25 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Deliberately conservative: paths are matched after `path.resolve`, so two
|
|
22
22
|
* different files never collide, and any ambiguity results in NOT evicting.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
*
|
|
24
|
+
* Bash-output eviction is additionally gated on token-budget pressure
|
|
25
|
+
* (`options.budgetPressure`, the fraction of the model's context window in
|
|
26
|
+
* use). At 0 pressure — the default — behaviour is identical to read-only GC.
|
|
27
|
+
* Bash output is not always recoverable, so it carries its own safeguards: a
|
|
28
|
+
* command that looks side-effecting is never elided (re-running it, as the
|
|
29
|
+
* stub invites, could repeat a destructive action), and below 80% pressure
|
|
30
|
+
* only large outputs are elided.
|
|
25
31
|
*/
|
|
26
32
|
import type { AgentMessage } from "@kolisachint/hoocode-agent-core";
|
|
27
33
|
export interface ContextGcOptions {
|
|
28
34
|
/** Working directory used to resolve relative tool path arguments. */
|
|
29
35
|
cwd: string;
|
|
36
|
+
/**
|
|
37
|
+
* Token-budget pressure in [0, 1] — the fraction of the model's context
|
|
38
|
+
* window currently in use. Absent or 0 reproduces read-only GC behaviour.
|
|
39
|
+
* Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.
|
|
40
|
+
*/
|
|
41
|
+
budgetPressure?: number;
|
|
30
42
|
}
|
|
31
43
|
/**
|
|
32
44
|
* Return a message array with superseded `read` results stubbed out. Returns the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-gc.d.ts","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"context-gc.d.ts","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAoBpE,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAuBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CA8FxG","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * (the newer read reflects newer state). The most recent read of each path is\n * always kept, and a read is only evicted when a *successful* later event\n * supersedes it — so a read whose edit failed (and which the model still needs\n * to retry) is never touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, the index of the last read and the last successful\n\t// mutate. A read at index i is superseded when a later read (index > i) or a\n\t// later successful mutate (index > i) exists for the same path.\n\tconst lastReadIndex = new Map<string, number>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tlastReadIndex.set(path, i);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst laterRead = (lastReadIndex.get(path) ?? -1) > i;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tif (!laterRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
package/dist/core/context-gc.js
CHANGED
|
@@ -20,14 +20,31 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Deliberately conservative: paths are matched after `path.resolve`, so two
|
|
22
22
|
* different files never collide, and any ambiguity results in NOT evicting.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
*
|
|
24
|
+
* Bash-output eviction is additionally gated on token-budget pressure
|
|
25
|
+
* (`options.budgetPressure`, the fraction of the model's context window in
|
|
26
|
+
* use). At 0 pressure — the default — behaviour is identical to read-only GC.
|
|
27
|
+
* Bash output is not always recoverable, so it carries its own safeguards: a
|
|
28
|
+
* command that looks side-effecting is never elided (re-running it, as the
|
|
29
|
+
* stub invites, could repeat a destructive action), and below 80% pressure
|
|
30
|
+
* only large outputs are elided.
|
|
25
31
|
*/
|
|
26
32
|
import { resolve } from "node:path";
|
|
27
33
|
/** Tool names whose result injects file contents into the transcript. */
|
|
28
34
|
const READ_TOOLS = new Set(["read"]);
|
|
29
35
|
/** Tool names that change a file, making a prior read of that path stale. */
|
|
30
36
|
const MUTATE_TOOLS = new Set(["edit", "write"]);
|
|
37
|
+
/**
|
|
38
|
+
* Commands whose bash output must never be elided, because the stub invites
|
|
39
|
+
* the model to re-run the command and re-running could repeat a destructive or
|
|
40
|
+
* state-changing action. Tested against the whole command string, and
|
|
41
|
+
* deliberately over-broad: a false positive merely keeps an output (safe),
|
|
42
|
+
* while the common verbose *read* commands (cat/grep/ls/find/git log/diff,
|
|
43
|
+
* test runners) intentionally do NOT match, so they stay evictable.
|
|
44
|
+
*/
|
|
45
|
+
const BASH_SIDE_EFFECT_PATTERN = /(\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\b|\bgit\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\b|\bsed\b[^|]*-i|>>?)/i;
|
|
46
|
+
/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */
|
|
47
|
+
const BASH_EVICTION_CHAR_THRESHOLD = 2000;
|
|
31
48
|
function isAssistant(m) {
|
|
32
49
|
return m.role === "assistant" && Array.isArray(m.content);
|
|
33
50
|
}
|
|
@@ -44,12 +61,20 @@ export function evictSupersededReads(messages, options) {
|
|
|
44
61
|
// display path (the original argument) for the stub text.
|
|
45
62
|
const resolvedPathByCallId = new Map();
|
|
46
63
|
const displayPathByResolved = new Map();
|
|
64
|
+
// Bash calls carry a `command`, not a `path`; capture it so the eviction
|
|
65
|
+
// pass can consult the side-effect guard by tool call id.
|
|
66
|
+
const bashCommandByCallId = new Map();
|
|
47
67
|
for (const m of messages) {
|
|
48
68
|
if (!isAssistant(m))
|
|
49
69
|
continue;
|
|
50
70
|
for (const block of m.content) {
|
|
51
71
|
if (block.type !== "toolCall" || !block.id)
|
|
52
72
|
continue;
|
|
73
|
+
if (block.name === "bash") {
|
|
74
|
+
const cmd = block.arguments?.command;
|
|
75
|
+
if (typeof cmd === "string" && cmd.length > 0)
|
|
76
|
+
bashCommandByCallId.set(block.id, cmd);
|
|
77
|
+
}
|
|
53
78
|
const rawPath = block.arguments?.path;
|
|
54
79
|
if (typeof rawPath !== "string" || rawPath.length === 0)
|
|
55
80
|
continue;
|
|
@@ -80,29 +105,55 @@ export function evictSupersededReads(messages, options) {
|
|
|
80
105
|
// Second pass: build the output, stubbing evicted reads. Keep every other
|
|
81
106
|
// message by reference; clone only the ones we rewrite so the persisted
|
|
82
107
|
// history (which may share these objects) is never mutated.
|
|
108
|
+
const pressure = options.budgetPressure ?? 0;
|
|
83
109
|
let changed = false;
|
|
84
110
|
const out = messages.map((m, i) => {
|
|
85
|
-
if (!isToolResult(m) || m.isError
|
|
86
|
-
return m;
|
|
87
|
-
const path = resolvedPathByCallId.get(m.toolCallId);
|
|
88
|
-
if (!path)
|
|
89
|
-
return m;
|
|
90
|
-
const laterRead = (lastReadIndex.get(path) ?? -1) > i;
|
|
91
|
-
const laterMutate = (lastMutateIndex.get(path) ?? -1) > i;
|
|
92
|
-
if (!laterRead && !laterMutate)
|
|
111
|
+
if (!isToolResult(m) || m.isError)
|
|
93
112
|
return m;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
// Superseded-read eviction — always on, pressure-independent.
|
|
114
|
+
if (READ_TOOLS.has(m.toolName)) {
|
|
115
|
+
const path = resolvedPathByCallId.get(m.toolCallId);
|
|
116
|
+
if (!path)
|
|
117
|
+
return m;
|
|
118
|
+
const laterRead = (lastReadIndex.get(path) ?? -1) > i;
|
|
119
|
+
const laterMutate = (lastMutateIndex.get(path) ?? -1) > i;
|
|
120
|
+
if (!laterRead && !laterMutate)
|
|
121
|
+
return m;
|
|
122
|
+
changed = true;
|
|
123
|
+
const display = displayPathByResolved.get(path) ?? path;
|
|
124
|
+
const reason = laterMutate ? "the file was modified after this read" : "the file was read again later";
|
|
125
|
+
return {
|
|
126
|
+
...m,
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Bash-output eviction — only under token-budget pressure (>= 0.6).
|
|
136
|
+
if (pressure >= 0.6 && m.toolName === "bash") {
|
|
137
|
+
const cmd = bashCommandByCallId.get(m.toolCallId) ?? "";
|
|
138
|
+
if (BASH_SIDE_EFFECT_PATTERN.test(cmd))
|
|
139
|
+
return m;
|
|
140
|
+
const textLen = m.content.filter((c) => c.type === "text").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);
|
|
141
|
+
// 60–79%: elide only large outputs. 80%+: elide unconditionally.
|
|
142
|
+
const shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;
|
|
143
|
+
if (shouldEvict) {
|
|
144
|
+
changed = true;
|
|
145
|
+
return {
|
|
146
|
+
...m,
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return m;
|
|
106
157
|
});
|
|
107
158
|
return changed ? out : messages;
|
|
108
159
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-gc.js","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * (the newer read reflects newer state). The most recent read of each path is\n * always kept, and a read is only evicted when a *successful* later event\n * supersedes it — so a read whose edit failed (and which the model still needs\n * to retry) is never touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n * Bash-output eviction is intentionally left out for now — that output is not\n * always recoverable, so it needs its own safeguards.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, the index of the last read and the last successful\n\t// mutate. A read at index i is superseded when a later read (index > i) or a\n\t// later successful mutate (index > i) exists for the same path.\n\tconst lastReadIndex = new Map<string, number>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tlastReadIndex.set(path, i);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError || !READ_TOOLS.has(m.toolName)) return m;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return m;\n\t\tconst laterRead = (lastReadIndex.get(path) ?? -1) > i;\n\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\tif (!laterRead && !laterMutate) return m;\n\t\tchanged = true;\n\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\treturn {\n\t\t\t...m,\n\t\t\tcontent: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context-gc.js","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * (the newer read reflects newer state). The most recent read of each path is\n * always kept, and a read is only evicted when a *successful* later event\n * supersedes it — so a read whose edit failed (and which the model still needs\n * to retry) is never touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, the index of the last read and the last successful\n\t// mutate. A read at index i is superseded when a later read (index > i) or a\n\t// later successful mutate (index > i) exists for the same path.\n\tconst lastReadIndex = new Map<string, number>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tlastReadIndex.set(path, i);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst laterRead = (lastReadIndex.get(path) ?? -1) > i;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tif (!laterRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
package/dist/core/sdk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the built-in `browser_run` + `browser_continue` tools, which drive the\n\t * `browsertools` deterministic browser engine (parent-in-the-loop). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead).\n\t */\n\tenableBrowserTools?: boolean;\n\t/**\n\t * Enable the built-in document tools — `DocRead`/`DocEdit`/`DocWrite` (extract\n\t * and lossless id-based editing) plus `DocScan`/`DocGrep`/`DocPeek` (the\n\t * token-sensitive discovery loop: outline, search, partial read). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead). They shell out to the `filetools` binary to\n\t * losslessly extract/edit structured/binary documents.\n\t */\n\tenableFileTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { AgentDefinition, AgentSource } from \"./agent-frontmatter.js\";\nexport { AgentRegistry, formatAgentsForPrompt, loadAgentRegistry } from \"./agent-registry.js\";\nexport * from \"./agent-session-runtime.js\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t\t...(options.enableBrowserTools ? [\"browser_run\", \"browser_continue\"] : []),\n\t\t...(options.enableFileTools ? [\"DocRead\", \"DocEdit\", \"DocWrite\", \"DocScan\", \"DocGrep\", \"DocPeek\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\treturn settingsManager.getContextGcEnabled() ? evictSupersededReads(transformed, { cwd }) : transformed;\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the built-in `browser_run` + `browser_continue` tools, which drive the\n\t * `browsertools` deterministic browser engine (parent-in-the-loop). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead).\n\t */\n\tenableBrowserTools?: boolean;\n\t/**\n\t * Enable the built-in document tools — `DocRead`/`DocEdit`/`DocWrite` (extract\n\t * and lossless id-based editing) plus `DocScan`/`DocGrep`/`DocPeek` (the\n\t * token-sensitive discovery loop: outline, search, partial read). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead). They shell out to the `filetools` binary to\n\t * losslessly extract/edit structured/binary documents.\n\t */\n\tenableFileTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { AgentDefinition, AgentSource } from \"./agent-frontmatter.js\";\nexport { AgentRegistry, formatAgentsForPrompt, loadAgentRegistry } from \"./agent-registry.js\";\nexport * from \"./agent-session-runtime.js\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t\t...(options.enableBrowserTools ? [\"browser_run\", \"browser_continue\"] : []),\n\t\t...(options.enableFileTools ? [\"DocRead\", \"DocEdit\", \"DocWrite\", \"DocScan\", \"DocGrep\", \"DocPeek\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
package/dist/core/sdk.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { Agent, convertToLlm, createBackgroundPlaceholderText, createBackgroundTaskMessage, } from "@kolisachint/hoocode-agent-core";
|
|
2
|
+
import { Agent, convertToLlm, createBackgroundPlaceholderText, createBackgroundTaskMessage, estimateContextTokens, } from "@kolisachint/hoocode-agent-core";
|
|
3
3
|
import { clampThinkingLevel, streamSimple } from "@kolisachint/hoocode-ai";
|
|
4
4
|
import { getAgentDir } from "../config.js";
|
|
5
5
|
import { AgentSession } from "./agent-session.js";
|
|
@@ -188,6 +188,26 @@ export async function createAgentSession(options = {}) {
|
|
|
188
188
|
});
|
|
189
189
|
};
|
|
190
190
|
const extensionRunnerRef = {};
|
|
191
|
+
// Token-budget pressure for context GC: the fraction of the active model's
|
|
192
|
+
// context window in use, measured from the real usage on the outgoing
|
|
193
|
+
// message copy. It latches to a high-water mark so that our own evictions —
|
|
194
|
+
// which shrink the next turn's measured usage — cannot oscillate a message
|
|
195
|
+
// in and out of the transcript and thrash the provider's prefix cache. A
|
|
196
|
+
// large drop (compaction, fork) resets the latch to the new, smaller size.
|
|
197
|
+
let budgetPressureHighWater = 0;
|
|
198
|
+
const getBudgetPressure = (contextMessages) => {
|
|
199
|
+
const contextWindow = agent.state.model?.contextWindow ?? 0;
|
|
200
|
+
if (contextWindow <= 0)
|
|
201
|
+
return 0;
|
|
202
|
+
const gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);
|
|
203
|
+
if (gauge > budgetPressureHighWater) {
|
|
204
|
+
budgetPressureHighWater = gauge; // rising usage — track it
|
|
205
|
+
}
|
|
206
|
+
else if (gauge < budgetPressureHighWater - 0.15) {
|
|
207
|
+
budgetPressureHighWater = gauge; // context collapsed — reset the latch
|
|
208
|
+
}
|
|
209
|
+
return budgetPressureHighWater;
|
|
210
|
+
};
|
|
191
211
|
agent = new Agent({
|
|
192
212
|
initialState: {
|
|
193
213
|
systemPrompt: "",
|
|
@@ -243,7 +263,9 @@ export async function createAgentSession(options = {}) {
|
|
|
243
263
|
transformContext: async (messages) => {
|
|
244
264
|
const runner = extensionRunnerRef.current;
|
|
245
265
|
const transformed = runner ? await runner.emitContext(messages) : messages;
|
|
246
|
-
|
|
266
|
+
if (!settingsManager.getContextGcEnabled())
|
|
267
|
+
return transformed;
|
|
268
|
+
return evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });
|
|
247
269
|
},
|
|
248
270
|
steeringMode: settingsManager.getSteeringMode(),
|
|
249
271
|
followUpMode: settingsManager.getFollowUpMode(),
|
package/dist/core/sdk.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the built-in `browser_run` + `browser_continue` tools, which drive the\n\t * `browsertools` deterministic browser engine (parent-in-the-loop). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead).\n\t */\n\tenableBrowserTools?: boolean;\n\t/**\n\t * Enable the built-in document tools — `DocRead`/`DocEdit`/`DocWrite` (extract\n\t * and lossless id-based editing) plus `DocScan`/`DocGrep`/`DocPeek` (the\n\t * token-sensitive discovery loop: outline, search, partial read). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead). They shell out to the `filetools` binary to\n\t * losslessly extract/edit structured/binary documents.\n\t */\n\tenableFileTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { AgentDefinition, AgentSource } from \"./agent-frontmatter.js\";\nexport { AgentRegistry, formatAgentsForPrompt, loadAgentRegistry } from \"./agent-registry.js\";\nexport * from \"./agent-session-runtime.js\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t\t...(options.enableBrowserTools ? [\"browser_run\", \"browser_continue\"] : []),\n\t\t...(options.enableFileTools ? [\"DocRead\", \"DocEdit\", \"DocWrite\", \"DocScan\", \"DocGrep\", \"DocPeek\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\treturn settingsManager.getContextGcEnabled() ? evictSupersededReads(transformed, { cwd }) : transformed;\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the built-in `browser_run` + `browser_continue` tools, which drive the\n\t * `browsertools` deterministic browser engine (parent-in-the-loop). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead).\n\t */\n\tenableBrowserTools?: boolean;\n\t/**\n\t * Enable the built-in document tools — `DocRead`/`DocEdit`/`DocWrite` (extract\n\t * and lossless id-based editing) plus `DocScan`/`DocGrep`/`DocPeek` (the\n\t * token-sensitive discovery loop: outline, search, partial read). Defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list them there instead). They shell out to the `filetools` binary to\n\t * losslessly extract/edit structured/binary documents.\n\t */\n\tenableFileTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { AgentDefinition, AgentSource } from \"./agent-frontmatter.js\";\nexport { AgentRegistry, formatAgentsForPrompt, loadAgentRegistry } from \"./agent-registry.js\";\nexport * from \"./agent-session-runtime.js\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t\t...(options.enableBrowserTools ? [\"browser_run\", \"browser_continue\"] : []),\n\t\t...(options.enableFileTools ? [\"DocRead\", \"DocEdit\", \"DocWrite\", \"DocScan\", \"DocGrep\", \"DocPeek\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask-options.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAe,YAAY,EAAuC,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"ask-options.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAe,YAAY,EAAuC,MAAM,gCAAgC,CAAC;AAgCrH,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAiHtD","sourcesContent":["/**\n * Options pane — the ask_options tool.\n *\n * The model calls this tool when it needs the user to make a decision before\n * continuing. Each question is shown in an inline options pane where the user\n * moves with up/down, advances with right, and may type a custom answer.\n */\n\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport type { AskQuestion, ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\nimport { LOOP_AUTO_CHANGED, LOOP_HALT } from \"./loop.js\";\n\nconst askOptionsSchema = Type.Object({\n\tquestions: Type.Array(\n\t\tType.Object({\n\t\t\tquestion: Type.String({ description: \"The question to ask the user.\" }),\n\t\t\tdetail: Type.Optional(Type.String({ description: \"Optional clarifying sub-text shown under the question.\" })),\n\t\t\toptions: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\tlabel: Type.String({ description: \"The option text; returned verbatim when chosen.\" }),\n\t\t\t\t\tdescription: Type.Optional(\n\t\t\t\t\t\tType.String({ description: \"Optional short description shown next to the option.\" }),\n\t\t\t\t\t),\n\t\t\t\t\trecommended: Type.Optional(\n\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\tdescription: \"When true, the option is marked '(recommended)' to help the user choose.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\t{ description: \"The options the user can choose from.\" },\n\t\t\t),\n\t\t\tallow_custom: Type.Optional(\n\t\t\t\tType.Boolean({\n\t\t\t\t\tdescription: \"When true, the user can type a free-form answer instead of choosing an option.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ description: \"One or more decisions to ask the user, in order.\" },\n\t),\n});\n\nexport function setupAskOptions(pi: ExtensionAPI): void {\n\t// Capture the latest context so the tool can reach the interactive UI.\n\tlet activeCtx: ExtensionContext | undefined;\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t});\n\n\t// Track whether an autonomous /loop is running so we never block on a human\n\t// who isn't there. The loop extension broadcasts this on the shared bus.\n\tlet autoLoopActive = false;\n\tpi.events.on(LOOP_AUTO_CHANGED, (data) => {\n\t\tautoLoopActive = !!(data as { active?: boolean })?.active;\n\t});\n\n\tpi.registerTool({\n\t\tname: \"ask_options\",\n\t\tlabel: \"Ask the user\",\n\t\tdescription:\n\t\t\t\"Ask the user to make one or more decisions before continuing. Each question is presented \" +\n\t\t\t\"in an interactive options pane where the user selects an option (or types a custom answer). \" +\n\t\t\t\"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. \" +\n\t\t\t\"Returns the user's answer for each question; if the user skips, no answers are returned.\",\n\t\tparameters: askOptionsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof askOptionsSchema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\tif (!activeCtx || !activeCtx.hasUI) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!params.questions.length) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No questions were provided.\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Autonomous loop: no human is watching, so never block on the pane.\n\t\t\t// Decide any question that carries a recommended default and proceed;\n\t\t\t// if any question lacks one, there is no safe default — halt the loop\n\t\t\t// and let the model report the blocker instead of guessing.\n\t\t\tif (autoLoopActive) {\n\t\t\t\tconst blockers = params.questions.filter((q) => !q.options.some((o) => o.recommended));\n\t\t\t\tif (blockers.length) {\n\t\t\t\t\tconst list = blockers.map((q) => ` • ${q.question}`).join(\"\\n\");\n\t\t\t\t\tpi.events.emit(LOOP_HALT, {\n\t\t\t\t\t\treason: `ask_options had ${blockers.length} question(s) with no recommended default.`,\n\t\t\t\t\t});\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t\t`Autonomous loop: no user is available to answer, and ${blockers.length} question(s) have ` +\n\t\t\t\t\t\t\t\t\t`no recommended default to fall back on:\\n${list}\\n\\n` +\n\t\t\t\t\t\t\t\t\t`The loop has been stopped. Do not guess — stop and report this blocker to the user, ` +\n\t\t\t\t\t\t\t\t\t`explaining what decision is needed and the options you were weighing.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst text = params.questions\n\t\t\t\t\t.map((q) => {\n\t\t\t\t\t\tconst rec = q.options.find((o) => o.recommended);\n\t\t\t\t\t\treturn `${q.question}\\n → ${rec?.label} (auto-selected recommended default; autonomous loop, no user present)`;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\\n\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst questions: AskQuestion[] = params.questions.map((q) => ({\n\t\t\t\tquestion: q.question,\n\t\t\t\tdetail: q.detail,\n\t\t\t\toptions: q.options.map((o) => ({ label: o.label, description: o.description, recommended: o.recommended })),\n\t\t\t\tallowCustom: q.allow_custom,\n\t\t\t}));\n\n\t\t\tconst answers = await activeCtx.ui.askOptions(questions, { signal });\n\n\t\t\tif (!answers) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"The user skipped the question(s) without answering. Ask how they would like to proceed.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = questions.map((q, i) => `${q.question}\\n → ${answers[i] ?? \"(no answer)\"}`).join(\"\\n\\n\");\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* moves with up/down, advances with right, and may type a custom answer.
|
|
7
7
|
*/
|
|
8
8
|
import { Type } from "typebox";
|
|
9
|
+
import { LOOP_AUTO_CHANGED, LOOP_HALT } from "./loop.js";
|
|
9
10
|
const askOptionsSchema = Type.Object({
|
|
10
11
|
questions: Type.Array(Type.Object({
|
|
11
12
|
question: Type.String({ description: "The question to ask the user." }),
|
|
@@ -28,6 +29,12 @@ export function setupAskOptions(pi) {
|
|
|
28
29
|
pi.on("session_start", (_event, ctx) => {
|
|
29
30
|
activeCtx = ctx;
|
|
30
31
|
});
|
|
32
|
+
// Track whether an autonomous /loop is running so we never block on a human
|
|
33
|
+
// who isn't there. The loop extension broadcasts this on the shared bus.
|
|
34
|
+
let autoLoopActive = false;
|
|
35
|
+
pi.events.on(LOOP_AUTO_CHANGED, (data) => {
|
|
36
|
+
autoLoopActive = !!data?.active;
|
|
37
|
+
});
|
|
31
38
|
pi.registerTool({
|
|
32
39
|
name: "ask_options",
|
|
33
40
|
label: "Ask the user",
|
|
@@ -54,6 +61,41 @@ export function setupAskOptions(pi) {
|
|
|
54
61
|
details: undefined,
|
|
55
62
|
};
|
|
56
63
|
}
|
|
64
|
+
// Autonomous loop: no human is watching, so never block on the pane.
|
|
65
|
+
// Decide any question that carries a recommended default and proceed;
|
|
66
|
+
// if any question lacks one, there is no safe default — halt the loop
|
|
67
|
+
// and let the model report the blocker instead of guessing.
|
|
68
|
+
if (autoLoopActive) {
|
|
69
|
+
const blockers = params.questions.filter((q) => !q.options.some((o) => o.recommended));
|
|
70
|
+
if (blockers.length) {
|
|
71
|
+
const list = blockers.map((q) => ` • ${q.question}`).join("\n");
|
|
72
|
+
pi.events.emit(LOOP_HALT, {
|
|
73
|
+
reason: `ask_options had ${blockers.length} question(s) with no recommended default.`,
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: `Autonomous loop: no user is available to answer, and ${blockers.length} question(s) have ` +
|
|
80
|
+
`no recommended default to fall back on:\n${list}\n\n` +
|
|
81
|
+
`The loop has been stopped. Do not guess — stop and report this blocker to the user, ` +
|
|
82
|
+
`explaining what decision is needed and the options you were weighing.`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
details: undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const text = params.questions
|
|
89
|
+
.map((q) => {
|
|
90
|
+
const rec = q.options.find((o) => o.recommended);
|
|
91
|
+
return `${q.question}\n → ${rec?.label} (auto-selected recommended default; autonomous loop, no user present)`;
|
|
92
|
+
})
|
|
93
|
+
.join("\n\n");
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text }],
|
|
96
|
+
details: undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
57
99
|
const questions = params.questions.map((q) => ({
|
|
58
100
|
question: q.question,
|
|
59
101
|
detail: q.detail,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask-options.js","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Options pane — the ask_options tool.\n *\n * The model calls this tool when it needs the user to make a decision before\n * continuing. Each question is shown in an inline options pane where the user\n * moves with up/down, advances with right, and may type a custom answer.\n */\n\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport type { AskQuestion, ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\n\nconst askOptionsSchema = Type.Object({\n\tquestions: Type.Array(\n\t\tType.Object({\n\t\t\tquestion: Type.String({ description: \"The question to ask the user.\" }),\n\t\t\tdetail: Type.Optional(Type.String({ description: \"Optional clarifying sub-text shown under the question.\" })),\n\t\t\toptions: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\tlabel: Type.String({ description: \"The option text; returned verbatim when chosen.\" }),\n\t\t\t\t\tdescription: Type.Optional(\n\t\t\t\t\t\tType.String({ description: \"Optional short description shown next to the option.\" }),\n\t\t\t\t\t),\n\t\t\t\t\trecommended: Type.Optional(\n\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\tdescription: \"When true, the option is marked '(recommended)' to help the user choose.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\t{ description: \"The options the user can choose from.\" },\n\t\t\t),\n\t\t\tallow_custom: Type.Optional(\n\t\t\t\tType.Boolean({\n\t\t\t\t\tdescription: \"When true, the user can type a free-form answer instead of choosing an option.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ description: \"One or more decisions to ask the user, in order.\" },\n\t),\n});\n\nexport function setupAskOptions(pi: ExtensionAPI): void {\n\t// Capture the latest context so the tool can reach the interactive UI.\n\tlet activeCtx: ExtensionContext | undefined;\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t});\n\n\tpi.registerTool({\n\t\tname: \"ask_options\",\n\t\tlabel: \"Ask the user\",\n\t\tdescription:\n\t\t\t\"Ask the user to make one or more decisions before continuing. Each question is presented \" +\n\t\t\t\"in an interactive options pane where the user selects an option (or types a custom answer). \" +\n\t\t\t\"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. \" +\n\t\t\t\"Returns the user's answer for each question; if the user skips, no answers are returned.\",\n\t\tparameters: askOptionsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof askOptionsSchema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\tif (!activeCtx || !activeCtx.hasUI) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!params.questions.length) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No questions were provided.\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst questions: AskQuestion[] = params.questions.map((q) => ({\n\t\t\t\tquestion: q.question,\n\t\t\t\tdetail: q.detail,\n\t\t\t\toptions: q.options.map((o) => ({ label: o.label, description: o.description, recommended: o.recommended })),\n\t\t\t\tallowCustom: q.allow_custom,\n\t\t\t}));\n\n\t\t\tconst answers = await activeCtx.ui.askOptions(questions, { signal });\n\n\t\t\tif (!answers) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"The user skipped the question(s) without answering. Ask how they would like to proceed.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = questions.map((q, i) => `${q.question}\\n → ${answers[i] ?? \"(no answer)\"}`).join(\"\\n\\n\");\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ask-options.js","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Options pane — the ask_options tool.\n *\n * The model calls this tool when it needs the user to make a decision before\n * continuing. Each question is shown in an inline options pane where the user\n * moves with up/down, advances with right, and may type a custom answer.\n */\n\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport type { AskQuestion, ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\nimport { LOOP_AUTO_CHANGED, LOOP_HALT } from \"./loop.js\";\n\nconst askOptionsSchema = Type.Object({\n\tquestions: Type.Array(\n\t\tType.Object({\n\t\t\tquestion: Type.String({ description: \"The question to ask the user.\" }),\n\t\t\tdetail: Type.Optional(Type.String({ description: \"Optional clarifying sub-text shown under the question.\" })),\n\t\t\toptions: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\tlabel: Type.String({ description: \"The option text; returned verbatim when chosen.\" }),\n\t\t\t\t\tdescription: Type.Optional(\n\t\t\t\t\t\tType.String({ description: \"Optional short description shown next to the option.\" }),\n\t\t\t\t\t),\n\t\t\t\t\trecommended: Type.Optional(\n\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\tdescription: \"When true, the option is marked '(recommended)' to help the user choose.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\t{ description: \"The options the user can choose from.\" },\n\t\t\t),\n\t\t\tallow_custom: Type.Optional(\n\t\t\t\tType.Boolean({\n\t\t\t\t\tdescription: \"When true, the user can type a free-form answer instead of choosing an option.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ description: \"One or more decisions to ask the user, in order.\" },\n\t),\n});\n\nexport function setupAskOptions(pi: ExtensionAPI): void {\n\t// Capture the latest context so the tool can reach the interactive UI.\n\tlet activeCtx: ExtensionContext | undefined;\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t});\n\n\t// Track whether an autonomous /loop is running so we never block on a human\n\t// who isn't there. The loop extension broadcasts this on the shared bus.\n\tlet autoLoopActive = false;\n\tpi.events.on(LOOP_AUTO_CHANGED, (data) => {\n\t\tautoLoopActive = !!(data as { active?: boolean })?.active;\n\t});\n\n\tpi.registerTool({\n\t\tname: \"ask_options\",\n\t\tlabel: \"Ask the user\",\n\t\tdescription:\n\t\t\t\"Ask the user to make one or more decisions before continuing. Each question is presented \" +\n\t\t\t\"in an interactive options pane where the user selects an option (or types a custom answer). \" +\n\t\t\t\"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. \" +\n\t\t\t\"Returns the user's answer for each question; if the user skips, no answers are returned.\",\n\t\tparameters: askOptionsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof askOptionsSchema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\tif (!activeCtx || !activeCtx.hasUI) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!params.questions.length) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No questions were provided.\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Autonomous loop: no human is watching, so never block on the pane.\n\t\t\t// Decide any question that carries a recommended default and proceed;\n\t\t\t// if any question lacks one, there is no safe default — halt the loop\n\t\t\t// and let the model report the blocker instead of guessing.\n\t\t\tif (autoLoopActive) {\n\t\t\t\tconst blockers = params.questions.filter((q) => !q.options.some((o) => o.recommended));\n\t\t\t\tif (blockers.length) {\n\t\t\t\t\tconst list = blockers.map((q) => ` • ${q.question}`).join(\"\\n\");\n\t\t\t\t\tpi.events.emit(LOOP_HALT, {\n\t\t\t\t\t\treason: `ask_options had ${blockers.length} question(s) with no recommended default.`,\n\t\t\t\t\t});\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t\t`Autonomous loop: no user is available to answer, and ${blockers.length} question(s) have ` +\n\t\t\t\t\t\t\t\t\t`no recommended default to fall back on:\\n${list}\\n\\n` +\n\t\t\t\t\t\t\t\t\t`The loop has been stopped. Do not guess — stop and report this blocker to the user, ` +\n\t\t\t\t\t\t\t\t\t`explaining what decision is needed and the options you were weighing.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst text = params.questions\n\t\t\t\t\t.map((q) => {\n\t\t\t\t\t\tconst rec = q.options.find((o) => o.recommended);\n\t\t\t\t\t\treturn `${q.question}\\n → ${rec?.label} (auto-selected recommended default; autonomous loop, no user present)`;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\\n\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst questions: AskQuestion[] = params.questions.map((q) => ({\n\t\t\t\tquestion: q.question,\n\t\t\t\tdetail: q.detail,\n\t\t\t\toptions: q.options.map((o) => ({ label: o.label, description: o.description, recommended: o.recommended })),\n\t\t\t\tallowCustom: q.allow_custom,\n\t\t\t}));\n\n\t\t\tconst answers = await activeCtx.ui.askOptions(questions, { signal });\n\n\t\t\tif (!answers) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"The user skipped the question(s) without answering. Ask how they would like to proceed.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = questions.map((q, i) => `${q.question}\\n → ${answers[i] ?? \"(no answer)\"}`).join(\"\\n\\n\");\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
@@ -11,5 +11,17 @@
|
|
|
11
11
|
* /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE
|
|
12
12
|
*/
|
|
13
13
|
import type { ExtensionAPI } from "../../core/extensions/types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Event-bus channel: the autonomous-loop active state changed.
|
|
16
|
+
* Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops
|
|
17
|
+
* so other extensions (e.g. ask_options) can adapt to running unattended.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LOOP_AUTO_CHANGED = "loop:auto-changed";
|
|
20
|
+
/**
|
|
21
|
+
* Event-bus channel: request to halt the autonomous loop.
|
|
22
|
+
* Payload: `{ reason: string }`. Sent by another extension when it hits a
|
|
23
|
+
* blocker that requires a human decision the loop cannot safely make on its own.
|
|
24
|
+
*/
|
|
25
|
+
export declare const LOOP_HALT = "loop:halt";
|
|
14
26
|
export declare function setupLoop(pi: ExtensionAPI): void;
|
|
15
27
|
//# sourceMappingURL=loop.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AAuCxC,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAoNhD","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tauto = null;\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tauto = null;\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tauto = null;\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'Schedule prompts via cron or run an autonomous loop. /loop \"<cron>\" <prompt> | /loop <5m|2h> <prompt> | /loop once \"<cron>\" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tauto = null;\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto = { remaining: maxTurns };\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AAOxC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,SAAS,cAAc,CAAC;AAkCrC,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAsOhD","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/**\n * Event-bus channel: the autonomous-loop active state changed.\n * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops\n * so other extensions (e.g. ask_options) can adapt to running unattended.\n */\nexport const LOOP_AUTO_CHANGED = \"loop:auto-changed\";\n\n/**\n * Event-bus channel: request to halt the autonomous loop.\n * Payload: `{ reason: string }`. Sent by another extension when it hits a\n * blocker that requires a human decision the loop cannot safely make on its own.\n */\nexport const LOOP_HALT = \"loop:halt\";\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\tlet activeCtx: ExtensionContext | undefined;\n\n\t/** Set the autonomous-loop state and broadcast the active flag on the bus. */\n\tfunction setAuto(next: { remaining: number } | null): void {\n\t\tconst was = auto !== null;\n\t\tauto = next;\n\t\tif (was !== (next !== null)) pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });\n\t}\n\n\t// Another extension (e.g. ask_options) hit a decision it cannot safely make\n\t// while unattended. Stop iterating and let the model report the blocker.\n\tpi.events.on(LOOP_HALT, (data) => {\n\t\tif (!auto) return;\n\t\tconst reason = (data as { reason?: string })?.reason?.trim() || \"a decision that needs the user.\";\n\t\tsetAuto(null);\n\t\tactiveCtx?.ui.notify(`Autonomous loop halted: ${reason}`, \"warning\");\n\t});\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tsetAuto(null);\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'Schedule prompts via cron or run an autonomous loop. /loop \"<cron>\" <prompt> | /loop <5m|2h> <prompt> | /loop once \"<cron>\" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tsetAuto(null);\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetAuto({ remaining: maxTurns });\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
@@ -16,6 +16,18 @@ import { defineTool } from "../../core/extensions/types.js";
|
|
|
16
16
|
import { TaskScheduler } from "../../core/scheduler.js";
|
|
17
17
|
const AUTO_LOOP_DONE_TOKEN = "LOOP_DONE";
|
|
18
18
|
const DEFAULT_AUTO_MAX_TURNS = 10;
|
|
19
|
+
/**
|
|
20
|
+
* Event-bus channel: the autonomous-loop active state changed.
|
|
21
|
+
* Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops
|
|
22
|
+
* so other extensions (e.g. ask_options) can adapt to running unattended.
|
|
23
|
+
*/
|
|
24
|
+
export const LOOP_AUTO_CHANGED = "loop:auto-changed";
|
|
25
|
+
/**
|
|
26
|
+
* Event-bus channel: request to halt the autonomous loop.
|
|
27
|
+
* Payload: `{ reason: string }`. Sent by another extension when it hits a
|
|
28
|
+
* blocker that requires a human decision the loop cannot safely make on its own.
|
|
29
|
+
*/
|
|
30
|
+
export const LOOP_HALT = "loop:halt";
|
|
19
31
|
/** Convert a simple interval token ("5m", "2h", "1d") to a 5-field cron, or null. */
|
|
20
32
|
function intervalToCron(token) {
|
|
21
33
|
const m = /^(\d+)(m|h|d)$/.exec(token.trim());
|
|
@@ -53,7 +65,25 @@ function assistantText(message) {
|
|
|
53
65
|
export function setupLoop(pi) {
|
|
54
66
|
let scheduler;
|
|
55
67
|
let auto = null;
|
|
68
|
+
let activeCtx;
|
|
69
|
+
/** Set the autonomous-loop state and broadcast the active flag on the bus. */
|
|
70
|
+
function setAuto(next) {
|
|
71
|
+
const was = auto !== null;
|
|
72
|
+
auto = next;
|
|
73
|
+
if (was !== (next !== null))
|
|
74
|
+
pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });
|
|
75
|
+
}
|
|
76
|
+
// Another extension (e.g. ask_options) hit a decision it cannot safely make
|
|
77
|
+
// while unattended. Stop iterating and let the model report the blocker.
|
|
78
|
+
pi.events.on(LOOP_HALT, (data) => {
|
|
79
|
+
if (!auto)
|
|
80
|
+
return;
|
|
81
|
+
const reason = data?.reason?.trim() || "a decision that needs the user.";
|
|
82
|
+
setAuto(null);
|
|
83
|
+
activeCtx?.ui.notify(`Autonomous loop halted: ${reason}`, "warning");
|
|
84
|
+
});
|
|
56
85
|
pi.on("session_start", (_event, ctx) => {
|
|
86
|
+
activeCtx = ctx;
|
|
57
87
|
if (scheduler)
|
|
58
88
|
return;
|
|
59
89
|
const isIdle = () => {
|
|
@@ -76,7 +106,7 @@ export function setupLoop(pi) {
|
|
|
76
106
|
});
|
|
77
107
|
pi.on("session_shutdown", () => {
|
|
78
108
|
scheduler?.stop();
|
|
79
|
-
|
|
109
|
+
setAuto(null);
|
|
80
110
|
});
|
|
81
111
|
// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.
|
|
82
112
|
pi.on("agent_end", (event, ctx) => {
|
|
@@ -87,12 +117,12 @@ export function setupLoop(pi) {
|
|
|
87
117
|
const last = [...event.messages].reverse().find((m) => m.role === "assistant");
|
|
88
118
|
const text = last ? assistantText(last) : "";
|
|
89
119
|
if (text.includes(AUTO_LOOP_DONE_TOKEN)) {
|
|
90
|
-
|
|
120
|
+
setAuto(null);
|
|
91
121
|
ctx.ui.notify("Autonomous loop complete.", "info");
|
|
92
122
|
return;
|
|
93
123
|
}
|
|
94
124
|
if (auto.remaining <= 0) {
|
|
95
|
-
|
|
125
|
+
setAuto(null);
|
|
96
126
|
ctx.ui.notify("Autonomous loop stopped: max turns reached.", "warning");
|
|
97
127
|
return;
|
|
98
128
|
}
|
|
@@ -173,7 +203,7 @@ export function setupLoop(pi) {
|
|
|
173
203
|
if (trimmed === "stop") {
|
|
174
204
|
const had = scheduler.list().length > 0 || auto !== null;
|
|
175
205
|
scheduler.clear();
|
|
176
|
-
|
|
206
|
+
setAuto(null);
|
|
177
207
|
ctx.ui.notify(had ? "Stopped all loops and scheduled tasks." : "Nothing to stop.", "info");
|
|
178
208
|
return;
|
|
179
209
|
}
|
|
@@ -198,7 +228,7 @@ export function setupLoop(pi) {
|
|
|
198
228
|
ctx.ui.notify("Usage: /loop auto [--max-turns N] <task>", "warning");
|
|
199
229
|
return;
|
|
200
230
|
}
|
|
201
|
-
|
|
231
|
+
setAuto({ remaining: maxTurns });
|
|
202
232
|
pi.sendUserMessage(`${rest}\n\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`, { deliverAs: "followUp" });
|
|
203
233
|
ctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, "info");
|
|
204
234
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tauto = null;\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tauto = null;\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tauto = null;\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'Schedule prompts via cron or run an autonomous loop. /loop \"<cron>\" <prompt> | /loop <5m|2h> <prompt> | /loop once \"<cron>\" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tauto = null;\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto = { remaining: maxTurns };\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/**\n * Event-bus channel: the autonomous-loop active state changed.\n * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops\n * so other extensions (e.g. ask_options) can adapt to running unattended.\n */\nexport const LOOP_AUTO_CHANGED = \"loop:auto-changed\";\n\n/**\n * Event-bus channel: request to halt the autonomous loop.\n * Payload: `{ reason: string }`. Sent by another extension when it hits a\n * blocker that requires a human decision the loop cannot safely make on its own.\n */\nexport const LOOP_HALT = \"loop:halt\";\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\tlet activeCtx: ExtensionContext | undefined;\n\n\t/** Set the autonomous-loop state and broadcast the active flag on the bus. */\n\tfunction setAuto(next: { remaining: number } | null): void {\n\t\tconst was = auto !== null;\n\t\tauto = next;\n\t\tif (was !== (next !== null)) pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });\n\t}\n\n\t// Another extension (e.g. ask_options) hit a decision it cannot safely make\n\t// while unattended. Stop iterating and let the model report the blocker.\n\tpi.events.on(LOOP_HALT, (data) => {\n\t\tif (!auto) return;\n\t\tconst reason = (data as { reason?: string })?.reason?.trim() || \"a decision that needs the user.\";\n\t\tsetAuto(null);\n\t\tactiveCtx?.ui.notify(`Autonomous loop halted: ${reason}`, \"warning\");\n\t});\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tsetAuto(null);\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'Schedule prompts via cron or run an autonomous loop. /loop \"<cron>\" <prompt> | /loop <5m|2h> <prompt> | /loop once \"<cron>\" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tsetAuto(null);\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetAuto({ remaining: maxTurns });\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolisachint/hoocode-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.132",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"hoocodeConfig": {
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"prepublishOnly": "npm run clean && npm run build"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@kolisachint/hoocode-agent-core": "^0.4.
|
|
49
|
-
"@kolisachint/hoocode-ai": "^0.4.
|
|
50
|
-
"@kolisachint/hoocode-tui": "^0.4.
|
|
48
|
+
"@kolisachint/hoocode-agent-core": "^0.4.132",
|
|
49
|
+
"@kolisachint/hoocode-ai": "^0.4.132",
|
|
50
|
+
"@kolisachint/hoocode-tui": "^0.4.132",
|
|
51
51
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
|
52
52
|
"chalk": "^5.5.0",
|
|
53
53
|
"cli-highlight": "^2.1.11",
|