@tyvm/knowhow 0.0.137 → 0.0.140
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/autodoc/chat-guide.md +1 -0
- package/package.json +5 -2
- package/scripts/build-for-node.sh +4 -4
- package/src/agents/base/base.ts +254 -29
- package/src/agents/patcher/patcher.ts +1 -1
- package/src/agents/researcher/researcher.ts +2 -2
- package/src/agents/tools/agentCall.ts +6 -0
- package/src/agents/tools/connectAgent.ts +353 -0
- package/src/agents/tools/index.ts +6 -0
- package/src/agents/tools/list.ts +48 -0
- package/src/agents/tools/observe.ts +245 -0
- package/src/agents/tools/replyToParent.ts +122 -0
- package/src/agents/tools/runGenerate.ts +209 -0
- package/src/agents/tools/sendAgentMessage.ts +138 -0
- package/src/agents/tools/startAgentTask.ts +92 -5
- package/src/agents/tools/waitForAgentCompleted.ts +269 -0
- package/src/ai.ts +1 -0
- package/src/chat/modules/AgentModule.ts +89 -27
- package/src/chat/modules/SessionsModule.ts +6 -10
- package/src/clients/contextLimits.ts +26 -0
- package/src/clients/pricing/anthropic.ts +12 -0
- package/src/commands/agent.ts +22 -0
- package/src/commands/agents.ts +83 -0
- package/src/commands/hash.ts +3 -2
- package/src/commands/misc.ts +6 -0
- package/src/commands/services.ts +14 -0
- package/src/generate.ts +25 -3
- package/src/services/AgentService.ts +21 -3
- package/src/services/AgentSyncFs.ts +56 -0
- package/src/services/AgentSyncKnowhowWeb.ts +1 -1
- package/src/services/EventService.ts +4 -1
- package/src/services/KnowhowClient.ts +7 -0
- package/src/services/LazyToolsService.ts +7 -3
- package/src/services/MinimalToolsService.ts +15 -7
- package/src/services/SyncedAgentWatcher.ts +2 -0
- package/src/services/SyncerService.ts +35 -6
- package/src/services/Tools.ts +43 -3
- package/src/services/modules/index.ts +127 -46
- package/src/types.ts +5 -0
- package/tests/agents/interrupt.test.ts +11 -18
- package/tests/agents/observe-base.test.ts +183 -0
- package/tests/agents/tools/connectAgent.test.ts +138 -0
- package/tests/agents/tools/observe.test.ts +179 -0
- package/tests/agents/tools/replyToParent.test.ts +125 -0
- package/tests/agents/tools/sendAgentMessage.test.ts +99 -0
- package/tests/agents/tools/waitForAgentCompleted.test.ts +147 -0
- package/tests/chat/detach-attach-rendering.test.ts +315 -0
- package/tests/clients/contextLimits.test.ts +23 -1
- package/tests/clients/pricing.test.ts +20 -1
- package/tests/services/Tools.test.ts +172 -9
- package/tests/unit/modules/moduleLoading.test.ts +69 -0
- package/ts_build/package.json +5 -2
- package/ts_build/src/agents/base/base.d.ts +24 -1
- package/ts_build/src/agents/base/base.js +111 -16
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/patcher/patcher.js +1 -1
- package/ts_build/src/agents/patcher/patcher.js.map +1 -1
- package/ts_build/src/agents/researcher/researcher.js +2 -2
- package/ts_build/src/agents/researcher/researcher.js.map +1 -1
- package/ts_build/src/agents/tools/agentCall.js.map +1 -1
- package/ts_build/src/agents/tools/connectAgent.d.ts +21 -0
- package/ts_build/src/agents/tools/connectAgent.js +284 -0
- package/ts_build/src/agents/tools/connectAgent.js.map +1 -0
- package/ts_build/src/agents/tools/index.d.ts +6 -0
- package/ts_build/src/agents/tools/index.js +6 -0
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +40 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/observe.d.ts +20 -0
- package/ts_build/src/agents/tools/observe.js +144 -0
- package/ts_build/src/agents/tools/observe.js.map +1 -0
- package/ts_build/src/agents/tools/replyToParent.d.ts +4 -0
- package/ts_build/src/agents/tools/replyToParent.js +118 -0
- package/ts_build/src/agents/tools/replyToParent.js.map +1 -0
- package/ts_build/src/agents/tools/runGenerate.d.ts +19 -0
- package/ts_build/src/agents/tools/runGenerate.js +138 -0
- package/ts_build/src/agents/tools/runGenerate.js.map +1 -0
- package/ts_build/src/agents/tools/sendAgentMessage.d.ts +11 -0
- package/ts_build/src/agents/tools/sendAgentMessage.js +118 -0
- package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -0
- package/ts_build/src/agents/tools/startAgentTask.d.ts +7 -0
- package/ts_build/src/agents/tools/startAgentTask.js +53 -6
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/agents/tools/waitForAgentCompleted.d.ts +10 -0
- package/ts_build/src/agents/tools/waitForAgentCompleted.js +229 -0
- package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -0
- package/ts_build/src/ai.d.ts +1 -0
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +3 -1
- package/ts_build/src/chat/modules/AgentModule.js +46 -21
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +1 -6
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +21 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/pricing/anthropic.d.ts +2 -0
- package/ts_build/src/clients/pricing/anthropic.js +9 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/commands/agent.js +11 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.js +61 -0
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/hash.js +1 -1
- package/ts_build/src/commands/hash.js.map +1 -1
- package/ts_build/src/commands/misc.js +2 -0
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/services.js +10 -0
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/generate.d.ts +2 -0
- package/ts_build/src/generate.js +11 -3
- package/ts_build/src/generate.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +4 -1
- package/ts_build/src/services/AgentService.js +9 -2
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
- package/ts_build/src/services/AgentSyncFs.js +35 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -1
- package/ts_build/src/services/EventService.d.ts +4 -1
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.js +3 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +2 -2
- package/ts_build/src/services/LazyToolsService.js +2 -2
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.d.ts +2 -2
- package/ts_build/src/services/MinimalToolsService.js +6 -6
- package/ts_build/src/services/MinimalToolsService.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +1 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/SyncerService.d.ts +2 -1
- package/ts_build/src/services/SyncerService.js +8 -2
- package/ts_build/src/services/SyncerService.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +6 -1
- package/ts_build/src/services/Tools.js +9 -3
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +1 -1
- package/ts_build/src/services/modules/index.js +67 -27
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +3 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/tests/agents/interrupt.test.d.ts +0 -1
- package/ts_build/tests/agents/interrupt.test.js +3 -13
- package/ts_build/tests/agents/interrupt.test.js.map +1 -1
- package/ts_build/tests/agents/observe-base.test.d.ts +1 -0
- package/ts_build/tests/agents/observe-base.test.js +149 -0
- package/ts_build/tests/agents/observe-base.test.js.map +1 -0
- package/ts_build/tests/agents/tools/connectAgent.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/connectAgent.test.js +142 -0
- package/ts_build/tests/agents/tools/connectAgent.test.js.map +1 -0
- package/ts_build/tests/agents/tools/observe.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/observe.test.js +148 -0
- package/ts_build/tests/agents/tools/observe.test.js.map +1 -0
- package/ts_build/tests/agents/tools/replyToParent.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/replyToParent.test.js +116 -0
- package/ts_build/tests/agents/tools/replyToParent.test.js.map +1 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.js +112 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.js.map +1 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js +147 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js.map +1 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.d.ts +1 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.js +168 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.js.map +1 -0
- package/ts_build/tests/clients/contextLimits.test.js +11 -1
- package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
- package/ts_build/tests/clients/pricing.test.js +10 -1
- package/ts_build/tests/clients/pricing.test.js.map +1 -1
- package/ts_build/tests/services/Tools.test.js +126 -8
- package/ts_build/tests/services/Tools.test.js.map +1 -1
- package/ts_build/tests/unit/modules/moduleLoading.test.js +84 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
package/autodoc/chat-guide.md
CHANGED
|
@@ -79,6 +79,7 @@ The following commands are available from the CLI modules shown in the code. (Ad
|
|
|
79
79
|
| `/pause` | Pause the attached agent | `agent:attached` |
|
|
80
80
|
| `/unpause` | Unpause the attached agent | `agent:attached` |
|
|
81
81
|
| `/kill` | Terminate the attached agent | `agent:attached` |
|
|
82
|
+
| `/compact` | Force the attached agent to summarize/compact its conversation now, dropping back to a much cheaper context size | `agent:attached` |
|
|
82
83
|
| `/detach` | Detach from the attached agent task (leaves agent running) | `agent:attached` |
|
|
83
84
|
| `/done` | Exit the current agent interaction (detaches) | `agent:attached` |
|
|
84
85
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyvm/knowhow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.140",
|
|
4
4
|
"description": "ai cli with plugins and agents",
|
|
5
5
|
"main": "ts_build/src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"knowhow": "bin/knowhow.js"
|
|
8
8
|
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
11
|
+
},
|
|
9
12
|
"scripts": {
|
|
10
13
|
"test": "jest --testTimeout 300000",
|
|
11
14
|
"test:debug": "node --inspect-brk ../../node_modules/jest/bin/jest.js --detectOpenHandles --forceExit --testTimeout 300000",
|
|
@@ -39,7 +42,7 @@
|
|
|
39
42
|
"@types/express": "^4.17.13",
|
|
40
43
|
"@types/jest": "^29.5.13",
|
|
41
44
|
"@types/mocha": "^10.0.8",
|
|
42
|
-
"@types/node": "^
|
|
45
|
+
"@types/node": "^24.0.0",
|
|
43
46
|
"@types/ws": "^8.18.1",
|
|
44
47
|
"jest": "^29.1.1",
|
|
45
48
|
"prettier": "2.6.2",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Example: npm run node:build 24
|
|
6
6
|
#
|
|
7
7
|
# This script:
|
|
8
|
-
# 1. Compiles TypeScript with Node
|
|
8
|
+
# 1. Compiles TypeScript with Node 24 (falls back to current node if unavailable)
|
|
9
9
|
# 2. Creates /tmp/knowhow-node-<major> with the compiled output
|
|
10
10
|
# 3. Symlinks the package globally for ALL installed nvm versions matching the target
|
|
11
11
|
#
|
|
@@ -31,9 +31,9 @@ PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
31
31
|
|
|
32
32
|
echo "📦 Package dir: $PACKAGE_DIR"
|
|
33
33
|
|
|
34
|
-
# --- Find Node
|
|
34
|
+
# --- Find Node 24 for compiling TypeScript ---
|
|
35
35
|
NODE20_BIN=""
|
|
36
|
-
for dir in "$HOME/.nvm/versions/node"/
|
|
36
|
+
for dir in "$HOME/.nvm/versions/node"/v24.*/bin; do
|
|
37
37
|
if [ -f "$dir/node" ]; then
|
|
38
38
|
NODE20_BIN="$dir/node"
|
|
39
39
|
break
|
|
@@ -41,7 +41,7 @@ for dir in "$HOME/.nvm/versions/node"/v20.*/bin; do
|
|
|
41
41
|
done
|
|
42
42
|
|
|
43
43
|
if [ -z "$NODE20_BIN" ]; then
|
|
44
|
-
echo "⚠️ Node
|
|
44
|
+
echo "⚠️ Node 24 not found via nvm, falling back to current node for TS compile"
|
|
45
45
|
NODE20_BIN="$(which node)"
|
|
46
46
|
fi
|
|
47
47
|
|
package/src/agents/base/base.ts
CHANGED
|
@@ -43,6 +43,41 @@ export interface ToolCallEvent {
|
|
|
43
43
|
functionResp: any;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* A source is anything that can (a) start producing data, (b) hand each datum to
|
|
48
|
+
* a callback, and (c) be torn down. This is the unified contract behind
|
|
49
|
+
* `agent.observe()` — tool polls, events, streams, promises, peer agent threads,
|
|
50
|
+
* file tails, etc. all become adapters that satisfy this shape.
|
|
51
|
+
*/
|
|
52
|
+
export interface ObservationSource {
|
|
53
|
+
/** Human-friendly label for logs / delivered messages. */
|
|
54
|
+
label: string;
|
|
55
|
+
/**
|
|
56
|
+
* Begin producing. Call `emit(datum)` for each piece of data. Return a
|
|
57
|
+
* teardown function (or void). May be sync or async.
|
|
58
|
+
*/
|
|
59
|
+
start(
|
|
60
|
+
emit: (datum: unknown) => void
|
|
61
|
+
): void | (() => void) | Promise<void | (() => void)>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ObserveOpts {
|
|
65
|
+
/** Only deliver when the datum changed since the last emit. Default true. */
|
|
66
|
+
onlyOnChange?: boolean;
|
|
67
|
+
/** Max number of updates before auto-stopping. Default 20. */
|
|
68
|
+
maxUpdates?: number;
|
|
69
|
+
/** Max total duration in ms before auto-stopping. Default 10 min. */
|
|
70
|
+
maxDurationMs?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ActiveObservation {
|
|
74
|
+
id: string;
|
|
75
|
+
label: string;
|
|
76
|
+
updates: number;
|
|
77
|
+
teardownPromise: Promise<void | (() => void)>;
|
|
78
|
+
expiry?: NodeJS.Timeout;
|
|
79
|
+
}
|
|
80
|
+
|
|
46
81
|
export abstract class BaseAgent implements IAgent {
|
|
47
82
|
abstract name: string;
|
|
48
83
|
abstract description: string;
|
|
@@ -63,25 +98,35 @@ export abstract class BaseAgent implements IAgent {
|
|
|
63
98
|
protected turnCount = 0;
|
|
64
99
|
protected totalCostUsd = 0;
|
|
65
100
|
protected currentThread = 0;
|
|
66
|
-
protected reasoningEffort: CompletionOptions["reasoning_effort"] | undefined =
|
|
101
|
+
protected reasoningEffort: CompletionOptions["reasoning_effort"] | undefined =
|
|
102
|
+
undefined;
|
|
67
103
|
protected summarizeReasoning: boolean | undefined = undefined;
|
|
68
104
|
protected totalInputTokens = 0;
|
|
69
105
|
protected totalOutputTokens = 0;
|
|
70
106
|
protected totalCacheReadTokens = 0;
|
|
71
107
|
protected totalCacheWriteTokens = 0;
|
|
72
108
|
|
|
109
|
+
// The real prompt/input token count reported by the most recent completion
|
|
110
|
+
// response (input + cache read + cache write). This reflects exactly how many
|
|
111
|
+
// tokens the model consumed for the last request and is far more accurate than
|
|
112
|
+
// the whitespace-based estimate in getMessagesLength(). Used to decide when to
|
|
113
|
+
// compact. 0 means "no real usage seen yet" (fall back to the estimate).
|
|
114
|
+
protected lastPromptTokens = 0;
|
|
115
|
+
|
|
73
116
|
protected compressThreshold = 30000;
|
|
74
117
|
protected compressMinMessages = 30;
|
|
75
118
|
|
|
119
|
+
// Set when the user manually requests compaction (e.g. via /compact while
|
|
120
|
+
// attached). Forces the next loop iteration to compress the conversation
|
|
121
|
+
// regardless of whether the token threshold has been reached.
|
|
122
|
+
protected _forceCompact = false;
|
|
123
|
+
|
|
76
124
|
// Interrupt support: resolves the currently awaited tool call or completion
|
|
77
125
|
private _interruptResolve: (() => void) | null = null;
|
|
78
126
|
// A monotonically-increasing token identifying the active interruptible window.
|
|
79
127
|
// Used to ensure a stale (slow) operation completing in the background cannot
|
|
80
128
|
// clobber a newer interruptible window's resolver.
|
|
81
129
|
private _interruptToken = 0;
|
|
82
|
-
// Set when interrupt() is called with no active window, so the next
|
|
83
|
-
// makeInterruptible fires immediately instead of dropping the interrupt.
|
|
84
|
-
private _pendingInterrupt = false;
|
|
85
130
|
|
|
86
131
|
protected threads = [] as Message[][];
|
|
87
132
|
|
|
@@ -91,6 +136,12 @@ export abstract class BaseAgent implements IAgent {
|
|
|
91
136
|
// Internal messages
|
|
92
137
|
protected pendingMessages = [] as Message[];
|
|
93
138
|
|
|
139
|
+
// Active observations (subscriptions) owned by this agent instance. Kept
|
|
140
|
+
// per-agent (not module scope) so stopObserving() on one agent can't clobber
|
|
141
|
+
// another agent's observers, and so newTask() can tear them all down.
|
|
142
|
+
protected _observations = new Map<string, ActiveObservation>();
|
|
143
|
+
private _observationSeq = 0;
|
|
144
|
+
|
|
94
145
|
protected taskBreakdown = "";
|
|
95
146
|
protected summaries = [] as string[];
|
|
96
147
|
protected currentTaskId: string | null = null;
|
|
@@ -178,11 +229,24 @@ export abstract class BaseAgent implements IAgent {
|
|
|
178
229
|
this.compressThreshold = threshold;
|
|
179
230
|
}
|
|
180
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Request that the conversation be compacted at the next loop iteration,
|
|
234
|
+
* regardless of whether the token threshold has been reached. Combined with
|
|
235
|
+
* an interrupt(), this lets a user force compaction on an attached agent
|
|
236
|
+
* mid-task (e.g. via the `/compact` chat command) to drop back to a much
|
|
237
|
+
* cheaper per-interaction context size.
|
|
238
|
+
*/
|
|
239
|
+
requestCompact() {
|
|
240
|
+
this._forceCompact = true;
|
|
241
|
+
}
|
|
242
|
+
|
|
181
243
|
/**
|
|
182
244
|
* Returns the effective compress threshold for the current model.
|
|
183
245
|
* If the user has manually set a custom threshold (different from the default 30k),
|
|
184
246
|
* that value is used as-is. Otherwise, the threshold is dynamically computed as
|
|
185
|
-
*
|
|
247
|
+
* 70% of the model's context window limit (or tiered-pricing threshold), falling
|
|
248
|
+
* back to DEFAULT_CONTEXT_LIMIT. The 70% factor leaves headroom so token-count
|
|
249
|
+
* lag doesn't push a request past the model/tier limit before compression fires.
|
|
186
250
|
*/
|
|
187
251
|
getCompressThreshold(): number {
|
|
188
252
|
if (this.compressThreshold !== DEFAULT_CONTEXT_LIMIT) {
|
|
@@ -194,7 +258,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
194
258
|
);
|
|
195
259
|
const contextLimit = result?.contextLimit ?? DEFAULT_CONTEXT_LIMIT;
|
|
196
260
|
const threshold = result?.threshold ?? contextLimit;
|
|
197
|
-
return Math.floor(threshold * 0.
|
|
261
|
+
return Math.floor(threshold * 0.7);
|
|
198
262
|
}
|
|
199
263
|
|
|
200
264
|
setMaxTurns(maxTurns: number | null) {
|
|
@@ -219,6 +283,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
219
283
|
this.totalOutputTokens = 0;
|
|
220
284
|
this.totalCacheReadTokens = 0;
|
|
221
285
|
this.totalCacheWriteTokens = 0;
|
|
286
|
+
this.lastPromptTokens = 0;
|
|
222
287
|
this.status = this.eventTypes.inProgress;
|
|
223
288
|
this.turnCount = 0;
|
|
224
289
|
this.startTimeMs = Date.now();
|
|
@@ -227,7 +292,11 @@ export abstract class BaseAgent implements IAgent {
|
|
|
227
292
|
// Reset interrupt state so a queued/stale interrupt from a prior task
|
|
228
293
|
// can't fire against the new task.
|
|
229
294
|
this._interruptResolve = null;
|
|
230
|
-
this.
|
|
295
|
+
this._forceCompact = false;
|
|
296
|
+
|
|
297
|
+
// Tear down any observers from a prior task so they can't leak or inject
|
|
298
|
+
// into the new task.
|
|
299
|
+
this.stopObserving(undefined, "new task started");
|
|
231
300
|
|
|
232
301
|
// Emit event for plugin integration
|
|
233
302
|
const id = taskId || this.startTimeMs.toString();
|
|
@@ -473,6 +542,18 @@ export abstract class BaseAgent implements IAgent {
|
|
|
473
542
|
const cacheWriteTokens =
|
|
474
543
|
usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
|
|
475
544
|
|
|
545
|
+
// Record the real prompt token count for this single completion (not the
|
|
546
|
+
// running total). This represents the full context the model processed for
|
|
547
|
+
// the request: fresh input + cache-read + cache-write tokens. It is used by
|
|
548
|
+
// the compaction check as an accurate replacement for the whitespace-based
|
|
549
|
+
// getMessagesLength() estimate. Only track it for "prompt-shaped" usage
|
|
550
|
+
// (i.e. when there are input tokens) so a usage-less interrupt stub doesn't
|
|
551
|
+
// reset it to 0.
|
|
552
|
+
const promptTokens = inputTokens + cacheReadTokens + cacheWriteTokens;
|
|
553
|
+
if (promptTokens > 0) {
|
|
554
|
+
this.lastPromptTokens = promptTokens;
|
|
555
|
+
}
|
|
556
|
+
|
|
476
557
|
this.totalInputTokens += inputTokens;
|
|
477
558
|
this.totalOutputTokens += outputTokens;
|
|
478
559
|
this.totalCacheReadTokens += cacheReadTokens;
|
|
@@ -575,7 +656,14 @@ export abstract class BaseAgent implements IAgent {
|
|
|
575
656
|
};
|
|
576
657
|
|
|
577
658
|
const { functionResp, toolMessages } = await this.makeInterruptible(
|
|
578
|
-
this
|
|
659
|
+
// Pass this agent in as the per-call context (`_ctx`) so tools can be
|
|
660
|
+
// self-referential: they can read `_ctx.caller` (this agent) and
|
|
661
|
+
// `_ctx.taskId` to, e.g., look up the current task, send pending
|
|
662
|
+
// messages, or subscribe to the caller's events.
|
|
663
|
+
this.tools.callTool(toolCall, this.getEnabledToolNames(), {
|
|
664
|
+
caller: this,
|
|
665
|
+
taskId: this.currentTaskId,
|
|
666
|
+
}),
|
|
579
667
|
interruptResult
|
|
580
668
|
);
|
|
581
669
|
|
|
@@ -771,13 +859,6 @@ export abstract class BaseAgent implements IAgent {
|
|
|
771
859
|
|
|
772
860
|
this._interruptResolve = () => finish(() => resolve(interruptValue));
|
|
773
861
|
|
|
774
|
-
// If an interrupt arrived while there was no active window, honor it now.
|
|
775
|
-
if (this._pendingInterrupt) {
|
|
776
|
-
this._pendingInterrupt = false;
|
|
777
|
-
finish(() => resolve(interruptValue));
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
862
|
promise
|
|
782
863
|
.then((result) => {
|
|
783
864
|
// If this window was already interrupted/settled, drop the stale
|
|
@@ -815,12 +896,13 @@ export abstract class BaseAgent implements IAgent {
|
|
|
815
896
|
this._interruptResolve();
|
|
816
897
|
} else {
|
|
817
898
|
// No active interruptible window right now (we're between the AI
|
|
818
|
-
// completion and the tool call, or in message processing).
|
|
819
|
-
// interrupt so
|
|
820
|
-
//
|
|
821
|
-
|
|
899
|
+
// completion and the tool call, or in message processing). There is
|
|
900
|
+
// nothing to interrupt, so we do NOT queue it — a queued interrupt
|
|
901
|
+
// would pre-empt the next tool call and drop its result. Any message
|
|
902
|
+
// supplied with the poke was already added as a pending user message
|
|
903
|
+
// above, so the agent will still see it on its next step.
|
|
822
904
|
this.log(
|
|
823
|
-
"No active interruptible operation
|
|
905
|
+
"No active interruptible operation to interrupt",
|
|
824
906
|
"warn"
|
|
825
907
|
);
|
|
826
908
|
}
|
|
@@ -917,8 +999,12 @@ export abstract class BaseAgent implements IAgent {
|
|
|
917
999
|
tools: this.getEnabledTools(),
|
|
918
1000
|
tool_choice: "auto",
|
|
919
1001
|
long_ttl_cache: this.runTime() > 300_000,
|
|
920
|
-
...(this.reasoningEffort !== undefined && {
|
|
921
|
-
|
|
1002
|
+
...(this.reasoningEffort !== undefined && {
|
|
1003
|
+
reasoning_effort: this.reasoningEffort,
|
|
1004
|
+
}),
|
|
1005
|
+
...(this.summarizeReasoning !== undefined && {
|
|
1006
|
+
reasoning_summary: this.summarizeReasoning,
|
|
1007
|
+
}),
|
|
922
1008
|
},
|
|
923
1009
|
{ interruptValue: interruptResponse }
|
|
924
1010
|
);
|
|
@@ -1109,18 +1195,37 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1109
1195
|
return firstMessage.content;
|
|
1110
1196
|
}
|
|
1111
1197
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1198
|
+
// Compress when either the token threshold is exceeded OR the user has
|
|
1199
|
+
// manually requested compaction (e.g. via /compact on an attached agent).
|
|
1200
|
+
// A manual request bypasses the token threshold but still respects the
|
|
1201
|
+
// minimum-message guard so we never try to compress a tiny conversation.
|
|
1202
|
+
//
|
|
1203
|
+
// The size check uses getContextTokenCount(), which prefers the REAL
|
|
1204
|
+
// prompt token count from the last completion response over the
|
|
1205
|
+
// whitespace-based estimate — so it compares real tokens against the
|
|
1206
|
+
// real (token-based) compress threshold.
|
|
1207
|
+
const contextTokens = this.getContextTokenCount(messages);
|
|
1208
|
+
const overThreshold =
|
|
1209
|
+
contextTokens > this.getCompressThreshold() &&
|
|
1210
|
+
messages.length > this.compressMinMessages;
|
|
1211
|
+
|
|
1212
|
+
if (overThreshold || this._forceCompact) {
|
|
1116
1213
|
const taskBreakdown = await this.getTaskBreakdown(messages);
|
|
1117
1214
|
this.log(
|
|
1118
|
-
|
|
1119
|
-
messages
|
|
1120
|
-
|
|
1215
|
+
this._forceCompact
|
|
1216
|
+
? `Compacting messages (manual /compact request): ${contextTokens} tokens`
|
|
1217
|
+
: `Compressing messages: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`
|
|
1121
1218
|
);
|
|
1219
|
+
this._forceCompact = false;
|
|
1122
1220
|
messages = await this.compressMessages(messages, startIndex, endIndex);
|
|
1123
1221
|
this.startNewThread(messages);
|
|
1222
|
+
// Reset the real-token tracker. compressMessages() ran a completion over
|
|
1223
|
+
// the FULL (pre-compression) thread, so lastPromptTokens now reflects
|
|
1224
|
+
// that large context — not the small compressed thread. Zeroing it makes
|
|
1225
|
+
// getContextTokenCount() fall back to the whitespace estimate of the new
|
|
1226
|
+
// (small) thread until the next real completion reports fresh usage,
|
|
1227
|
+
// preventing an immediate false re-compaction on the following iteration.
|
|
1228
|
+
this.lastPromptTokens = 0;
|
|
1124
1229
|
}
|
|
1125
1230
|
|
|
1126
1231
|
if (["assistant", "tool"].includes(messages[messages.length - 1].role)) {
|
|
@@ -1284,10 +1389,130 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1284
1389
|
this.events.emit(this.eventTypes.userSay, message.content);
|
|
1285
1390
|
}
|
|
1286
1391
|
|
|
1392
|
+
/**
|
|
1393
|
+
* Subscribe to any data-yielding source and stream its data into this agent's
|
|
1394
|
+
* loop as non-blocking pending messages. This is the unified subscription
|
|
1395
|
+
* primitive behind the `observe` tool — a tool poll, an event, a stream, a
|
|
1396
|
+
* peer agent's thread, a file tail, etc. all become `ObservationSource`
|
|
1397
|
+
* adapters feeding the same sink (`addPendingMessage`).
|
|
1398
|
+
*
|
|
1399
|
+
* Returns an observation id that can be passed to `stopObserving`. Auto-expires
|
|
1400
|
+
* after `maxUpdates` or `maxDurationMs` so a forgotten observer never leaks.
|
|
1401
|
+
*/
|
|
1402
|
+
observe(source: ObservationSource, opts: ObserveOpts = {}): string {
|
|
1403
|
+
const {
|
|
1404
|
+
onlyOnChange = true,
|
|
1405
|
+
maxUpdates = 20,
|
|
1406
|
+
maxDurationMs = 10 * 60 * 1000,
|
|
1407
|
+
} = opts;
|
|
1408
|
+
|
|
1409
|
+
this._observationSeq += 1;
|
|
1410
|
+
// Keep the id free of characters that would break id-extraction regexes
|
|
1411
|
+
// (labels can contain parens, spaces, JSON, etc).
|
|
1412
|
+
const safeLabel = source.label.replace(/[^A-Za-z0-9]+/g, "_").slice(0, 24);
|
|
1413
|
+
const id = `obs_${safeLabel}_${Date.now()}_${this._observationSeq}`;
|
|
1414
|
+
let last: string | undefined;
|
|
1415
|
+
|
|
1416
|
+
const emit = (datum: unknown) => {
|
|
1417
|
+
// status gate: don't inject once the agent is done
|
|
1418
|
+
if (this.status === this.eventTypes.done) {
|
|
1419
|
+
this.stopObserving(id);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const str = typeof datum === "string" ? datum : JSON.stringify(datum);
|
|
1423
|
+
if (onlyOnChange && str === last) return; // dedupe noise
|
|
1424
|
+
last = str;
|
|
1425
|
+
const obs = this._observations.get(id);
|
|
1426
|
+
if (!obs) return;
|
|
1427
|
+
obs.updates += 1;
|
|
1428
|
+
this.addPendingMessage({
|
|
1429
|
+
role: "user",
|
|
1430
|
+
content: `[observe:${id}] ${source.label} (update ${obs.updates}/${maxUpdates}):\n${str}`,
|
|
1431
|
+
});
|
|
1432
|
+
if (obs.updates >= maxUpdates) {
|
|
1433
|
+
this.stopObserving(id, "max updates reached");
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
const teardownPromise = Promise.resolve(source.start(emit)).catch((e) => {
|
|
1438
|
+
this.log(`observe(${source.label}) start failed: ${e?.message ?? e}`, "warn");
|
|
1439
|
+
return undefined;
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
const expiry = maxDurationMs
|
|
1443
|
+
? setTimeout(() => this.stopObserving(id, "max duration reached"), maxDurationMs)
|
|
1444
|
+
: undefined;
|
|
1445
|
+
if (expiry && typeof (expiry as any).unref === "function") {
|
|
1446
|
+
(expiry as any).unref();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
this._observations.set(id, {
|
|
1450
|
+
id,
|
|
1451
|
+
label: source.label,
|
|
1452
|
+
updates: 0,
|
|
1453
|
+
teardownPromise,
|
|
1454
|
+
expiry,
|
|
1455
|
+
});
|
|
1456
|
+
return id;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Cancel one observation (by id) or all of them (no id). Companion to
|
|
1461
|
+
* `observe`. Runs each source's teardown function if it returned one.
|
|
1462
|
+
*/
|
|
1463
|
+
stopObserving(id?: string, reason = "stopped"): string {
|
|
1464
|
+
const ids = id ? [id] : [...this._observations.keys()];
|
|
1465
|
+
let stopped = 0;
|
|
1466
|
+
for (const oid of ids) {
|
|
1467
|
+
const obs = this._observations.get(oid);
|
|
1468
|
+
if (!obs) continue;
|
|
1469
|
+
if (obs.expiry) clearTimeout(obs.expiry);
|
|
1470
|
+
Promise.resolve(obs.teardownPromise)
|
|
1471
|
+
.then((fn) => {
|
|
1472
|
+
if (typeof fn === "function") fn();
|
|
1473
|
+
})
|
|
1474
|
+
.catch(() => {});
|
|
1475
|
+
this._observations.delete(oid);
|
|
1476
|
+
stopped += 1;
|
|
1477
|
+
}
|
|
1478
|
+
return id
|
|
1479
|
+
? stopped
|
|
1480
|
+
? `Stopped observer ${id} (${reason}).`
|
|
1481
|
+
: `No active observer found with id: ${id}.`
|
|
1482
|
+
: `Stopped ${stopped} active observer(s) (${reason}).`;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1287
1485
|
getMessagesLength(messages: Message[]) {
|
|
1288
1486
|
return JSON.stringify(messages).split(" ").length;
|
|
1289
1487
|
}
|
|
1290
1488
|
|
|
1489
|
+
/**
|
|
1490
|
+
* Returns the best available estimate of how many tokens the current
|
|
1491
|
+
* conversation occupies in the model's context window, for the purpose of
|
|
1492
|
+
* deciding when to compact.
|
|
1493
|
+
*
|
|
1494
|
+
* Prefers the REAL prompt token count reported by the most recent completion
|
|
1495
|
+
* response (lastPromptTokens) — this is exactly how many tokens the model
|
|
1496
|
+
* consumed for the last request (fresh input + cache read + cache write), so
|
|
1497
|
+
* it is far more accurate than the whitespace-based getMessagesLength()
|
|
1498
|
+
* heuristic (which counts space-separated chunks of the JSON-stringified
|
|
1499
|
+
* messages and can be off by a large factor).
|
|
1500
|
+
*
|
|
1501
|
+
* Falls back to the whitespace estimate when no real usage has been observed
|
|
1502
|
+
* yet (e.g. before the first completion, or when a provider omits usage).
|
|
1503
|
+
*
|
|
1504
|
+
* Note: lastPromptTokens reflects the context size at the START of the last
|
|
1505
|
+
* request, so it lags by whatever the model generated + tool results appended
|
|
1506
|
+
* since then. That lag is intentionally covered by the 70% headroom baked
|
|
1507
|
+
* into getCompressThreshold().
|
|
1508
|
+
*/
|
|
1509
|
+
getContextTokenCount(messages: Message[]): number {
|
|
1510
|
+
if (this.lastPromptTokens > 0) {
|
|
1511
|
+
return this.lastPromptTokens;
|
|
1512
|
+
}
|
|
1513
|
+
return this.getMessagesLength(messages);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1291
1516
|
/**
|
|
1292
1517
|
* Detects whether tool call arguments appear truncated due to hitting the output token limit.
|
|
1293
1518
|
* Two signals are checked:
|
|
@@ -80,7 +80,7 @@ export class PatchingAgent extends BaseAgent {
|
|
|
80
80
|
|
|
81
81
|
this.setModelPreferences([
|
|
82
82
|
{ model: Models.anthropic.Sonnet4_6, provider: "anthropic" },
|
|
83
|
-
{ model: Models.openai.
|
|
83
|
+
{ model: Models.openai.GPT_56_Sol, provider: "openai" },
|
|
84
84
|
{ model: Models.xai.GrokCodeFast, provider: "xai" },
|
|
85
85
|
]);
|
|
86
86
|
}
|
|
@@ -13,9 +13,9 @@ export class ResearcherAgent extends BaseAgent {
|
|
|
13
13
|
// fallbacks, an unregistered "google" provider causes the health-check loop
|
|
14
14
|
// to immediately throw "We have exhausted all model preferences."
|
|
15
15
|
this.setModelPreferences([
|
|
16
|
-
{ model: Models.
|
|
16
|
+
{ model: Models.openai.GPT_56_Terra, provider: "openai" },
|
|
17
17
|
{ model: Models.anthropic.Sonnet4_6, provider: "anthropic" },
|
|
18
|
-
{ model: Models.
|
|
18
|
+
{ model: Models.google.Gemini_35_Flash_Lite, provider: "google" },
|
|
19
19
|
]);
|
|
20
20
|
this.disableTool("patchFile");
|
|
21
21
|
this.disableTool("writeFile");
|
|
@@ -2,6 +2,12 @@ import { getConfig } from "../../config";
|
|
|
2
2
|
import { services, ToolsService } from "../../services";
|
|
3
3
|
import { getEnabledPlugins } from "../../types";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Ask another registered agent a question. Resolves with a structured
|
|
7
|
+
* `{ answer, costUsd }` object (via `AgentService.callAgent`) so callers — most
|
|
8
|
+
* importantly the script sandbox — can account the subagent's spend against a
|
|
9
|
+
* budget. `answer` is the target agent's final response.
|
|
10
|
+
*/
|
|
5
11
|
export async function agentCall(agentName: string, userInput: string) {
|
|
6
12
|
return new Promise(async (resolve, reject) => {
|
|
7
13
|
const config = await getConfig();
|