omk-agent-core 0.98.0 → 0.98.2
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/dist/harness/agent-harness.d.ts +29 -14
- package/dist/harness/agent-harness.d.ts.map +1 -1
- package/dist/harness/agent-harness.js +409 -453
- package/dist/harness/agent-harness.js.map +1 -1
- package/dist/harness/compaction/branch-summarization.d.ts +5 -1
- package/dist/harness/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/harness/compaction/branch-summarization.js +3 -3
- package/dist/harness/compaction/branch-summarization.js.map +1 -1
- package/dist/harness/compaction/compaction.d.ts +8 -4
- package/dist/harness/compaction/compaction.d.ts.map +1 -1
- package/dist/harness/compaction/compaction.js +12 -97
- package/dist/harness/compaction/compaction.js.map +1 -1
- package/dist/harness/compaction/operation.d.ts +13 -0
- package/dist/harness/compaction/operation.d.ts.map +1 -0
- package/dist/harness/compaction/operation.js +2 -0
- package/dist/harness/compaction/operation.js.map +1 -0
- package/dist/harness/compaction/summarization-prompts.d.ts +5 -0
- package/dist/harness/compaction/summarization-prompts.d.ts.map +1 -0
- package/dist/harness/compaction/summarization-prompts.js +88 -0
- package/dist/harness/compaction/summarization-prompts.js.map +1 -0
- package/dist/harness/errors.d.ts +15 -0
- package/dist/harness/errors.d.ts.map +1 -0
- package/dist/harness/errors.js +32 -0
- package/dist/harness/errors.js.map +1 -0
- package/dist/harness/harness-session.d.ts +52 -0
- package/dist/harness/harness-session.d.ts.map +1 -0
- package/dist/harness/harness-session.js +113 -0
- package/dist/harness/harness-session.js.map +1 -0
- package/dist/harness/messages.d.ts +3 -1
- package/dist/harness/messages.d.ts.map +1 -1
- package/dist/harness/messages.js +26 -0
- package/dist/harness/messages.js.map +1 -1
- package/dist/harness/name-validation.d.ts +2 -0
- package/dist/harness/name-validation.d.ts.map +1 -0
- package/dist/harness/name-validation.js +11 -0
- package/dist/harness/name-validation.js.map +1 -0
- package/dist/harness/operation-lifecycle-controller.d.ts +68 -0
- package/dist/harness/operation-lifecycle-controller.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-controller.js +199 -0
- package/dist/harness/operation-lifecycle-controller.js.map +1 -0
- package/dist/harness/operation-lifecycle-reducer.d.ts +19 -0
- package/dist/harness/operation-lifecycle-reducer.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-reducer.js +201 -0
- package/dist/harness/operation-lifecycle-reducer.js.map +1 -0
- package/dist/harness/operation-lifecycle-types.d.ts +130 -0
- package/dist/harness/operation-lifecycle-types.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-types.js +34 -0
- package/dist/harness/operation-lifecycle-types.js.map +1 -0
- package/dist/harness/operation-outcome.d.ts +71 -0
- package/dist/harness/operation-outcome.d.ts.map +1 -0
- package/dist/harness/operation-outcome.js +131 -0
- package/dist/harness/operation-outcome.js.map +1 -0
- package/dist/harness/session-write-coordinator.d.ts +76 -0
- package/dist/harness/session-write-coordinator.d.ts.map +1 -0
- package/dist/harness/session-write-coordinator.js +126 -0
- package/dist/harness/session-write-coordinator.js.map +1 -0
- package/dist/harness/stream-options.d.ts +31 -0
- package/dist/harness/stream-options.d.ts.map +1 -0
- package/dist/harness/stream-options.js +66 -0
- package/dist/harness/stream-options.js.map +1 -0
- package/dist/harness/subscriber-fanout.d.ts +37 -0
- package/dist/harness/subscriber-fanout.d.ts.map +1 -0
- package/dist/harness/subscriber-fanout.js +73 -0
- package/dist/harness/subscriber-fanout.js.map +1 -0
- package/dist/harness/summarization-retry.d.ts +29 -0
- package/dist/harness/summarization-retry.d.ts.map +1 -0
- package/dist/harness/summarization-retry.js +20 -0
- package/dist/harness/summarization-retry.js.map +1 -0
- package/dist/harness/tree-navigation.d.ts +45 -0
- package/dist/harness/tree-navigation.d.ts.map +1 -0
- package/dist/harness/tree-navigation.js +59 -0
- package/dist/harness/tree-navigation.js.map +1 -0
- package/dist/harness/types.d.ts +69 -43
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js +1 -32
- package/dist/harness/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,138 +1,33 @@
|
|
|
1
|
-
import { streamSimple } from "omk-ai";
|
|
2
|
-
import { runAgentLoop } from "../agent-loop.js";
|
|
3
|
-
import { collectEntriesForBranchSummary
|
|
4
|
-
import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from "./compaction/compaction.js";
|
|
5
|
-
import {
|
|
1
|
+
import { isContextOverflow, streamSimple, } from "omk-ai";
|
|
2
|
+
import { runAgentLoop, runAgentLoopContinue } from "../agent-loop.js";
|
|
3
|
+
import { collectEntriesForBranchSummary } from "./compaction/branch-summarization.js";
|
|
4
|
+
import { compact, DEFAULT_COMPACTION_SETTINGS, estimateContextTokens, prepareCompaction, shouldCompact, } from "./compaction/compaction.js";
|
|
5
|
+
import { HarnessSessionFacade } from "./harness-session.js";
|
|
6
|
+
import { convertToLlm, createFailureMessage, createUserMessage } from "./messages.js";
|
|
7
|
+
import { findDuplicateNames } from "./name-validation.js";
|
|
8
|
+
import { OperationLifecycleController, } from "./operation-lifecycle-controller.js";
|
|
9
|
+
import { PROMPT_FAMILY_KINDS, } from "./operation-lifecycle-types.js";
|
|
10
|
+
import { classifyAssistantOutcome, classifyAttemptFailure, classifyAttemptOutcome, classifyNavigateTreeOutcome, combineBoundaryErrors, normalizeHarnessError, resolveOperationFailure, resolveOperationOutcome, } from "./operation-outcome.js";
|
|
6
11
|
import { formatPromptTemplateInvocation } from "./prompt-templates.js";
|
|
12
|
+
import { uuidv7 } from "./session/uuid.js";
|
|
13
|
+
import { SessionWriteCoordinator } from "./session-write-coordinator.js";
|
|
7
14
|
import { formatSkillInvocation } from "./skills.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return { role: "user", content, timestamp: Date.now() };
|
|
14
|
-
}
|
|
15
|
-
function createFailureMessage(model, error, aborted) {
|
|
16
|
-
return {
|
|
17
|
-
role: "assistant",
|
|
18
|
-
content: [{ type: "text", text: "" }],
|
|
19
|
-
api: model.api,
|
|
20
|
-
provider: model.provider,
|
|
21
|
-
model: model.id,
|
|
22
|
-
stopReason: aborted ? "aborted" : "error",
|
|
23
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
24
|
-
timestamp: Date.now(),
|
|
25
|
-
usage: {
|
|
26
|
-
input: 0,
|
|
27
|
-
output: 0,
|
|
28
|
-
cacheRead: 0,
|
|
29
|
-
cacheWrite: 0,
|
|
30
|
-
totalTokens: 0,
|
|
31
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function cloneStreamOptions(streamOptions) {
|
|
36
|
-
return {
|
|
37
|
-
...streamOptions,
|
|
38
|
-
headers: streamOptions?.headers ? { ...streamOptions.headers } : undefined,
|
|
39
|
-
metadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function mergeHeaders(...headers) {
|
|
43
|
-
const merged = {};
|
|
44
|
-
let hasHeaders = false;
|
|
45
|
-
for (const entry of headers) {
|
|
46
|
-
if (!entry)
|
|
47
|
-
continue;
|
|
48
|
-
Object.assign(merged, entry);
|
|
49
|
-
hasHeaders = true;
|
|
50
|
-
}
|
|
51
|
-
return hasHeaders ? merged : undefined;
|
|
52
|
-
}
|
|
53
|
-
function findDuplicateNames(names) {
|
|
54
|
-
const seen = new Set();
|
|
55
|
-
const duplicates = new Set();
|
|
56
|
-
for (const name of names) {
|
|
57
|
-
if (seen.has(name))
|
|
58
|
-
duplicates.add(name);
|
|
59
|
-
seen.add(name);
|
|
60
|
-
}
|
|
61
|
-
return [...duplicates];
|
|
62
|
-
}
|
|
63
|
-
function applyStreamOptionsPatch(base, patch) {
|
|
64
|
-
const result = cloneStreamOptions(base);
|
|
65
|
-
if (!patch)
|
|
66
|
-
return result;
|
|
67
|
-
if (Object.hasOwn(patch, "transport"))
|
|
68
|
-
result.transport = patch.transport;
|
|
69
|
-
if (Object.hasOwn(patch, "timeoutMs"))
|
|
70
|
-
result.timeoutMs = patch.timeoutMs;
|
|
71
|
-
if (Object.hasOwn(patch, "maxRetries"))
|
|
72
|
-
result.maxRetries = patch.maxRetries;
|
|
73
|
-
if (Object.hasOwn(patch, "maxRetryDelayMs"))
|
|
74
|
-
result.maxRetryDelayMs = patch.maxRetryDelayMs;
|
|
75
|
-
if (Object.hasOwn(patch, "cacheRetention"))
|
|
76
|
-
result.cacheRetention = patch.cacheRetention;
|
|
77
|
-
if (Object.hasOwn(patch, "headers")) {
|
|
78
|
-
if (patch.headers === undefined) {
|
|
79
|
-
result.headers = undefined;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
const headers = { ...(result.headers ?? {}) };
|
|
83
|
-
for (const [key, value] of Object.entries(patch.headers)) {
|
|
84
|
-
if (value === undefined)
|
|
85
|
-
delete headers[key];
|
|
86
|
-
else
|
|
87
|
-
headers[key] = value;
|
|
88
|
-
}
|
|
89
|
-
result.headers = Object.keys(headers).length > 0 ? headers : undefined;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (Object.hasOwn(patch, "metadata")) {
|
|
93
|
-
if (patch.metadata === undefined) {
|
|
94
|
-
result.metadata = undefined;
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
const metadata = { ...(result.metadata ?? {}) };
|
|
98
|
-
for (const [key, value] of Object.entries(patch.metadata)) {
|
|
99
|
-
if (value === undefined)
|
|
100
|
-
delete metadata[key];
|
|
101
|
-
else
|
|
102
|
-
metadata[key] = value;
|
|
103
|
-
}
|
|
104
|
-
result.metadata = Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
const SUBSCRIBER_EVENT_TYPE = "*";
|
|
110
|
-
function normalizeHarnessError(error, fallbackCode) {
|
|
111
|
-
if (error instanceof AgentHarnessError)
|
|
112
|
-
return error;
|
|
113
|
-
const cause = toError(error);
|
|
114
|
-
if (cause instanceof SessionError)
|
|
115
|
-
return new AgentHarnessError("session", cause.message, cause);
|
|
116
|
-
if (cause instanceof CompactionError)
|
|
117
|
-
return new AgentHarnessError("compaction", cause.message, cause);
|
|
118
|
-
if (cause instanceof BranchSummaryError)
|
|
119
|
-
return new AgentHarnessError("branch_summary", cause.message, cause);
|
|
120
|
-
return new AgentHarnessError(fallbackCode, cause.message, cause);
|
|
121
|
-
}
|
|
122
|
-
function normalizeHookError(error) {
|
|
123
|
-
return normalizeHarnessError(error, "hook");
|
|
124
|
-
}
|
|
15
|
+
import { applyStreamOptionsPatch, cloneStreamOptions, mergeHeaders } from "./stream-options.js";
|
|
16
|
+
import { SubscriberFanout } from "./subscriber-fanout.js";
|
|
17
|
+
import { createSummarizationRetry } from "./summarization-retry.js";
|
|
18
|
+
import { resolveNavigationTarget, runBranchSummary } from "./tree-navigation.js";
|
|
19
|
+
import { AgentHarnessError, toError } from "./types.js";
|
|
125
20
|
export class AgentHarness {
|
|
126
21
|
env;
|
|
127
22
|
session;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
pendingSessionWrites = [];
|
|
23
|
+
sessionFacade;
|
|
24
|
+
lifecycle;
|
|
25
|
+
sessionWrites;
|
|
132
26
|
model;
|
|
133
27
|
thinkingLevel;
|
|
134
28
|
systemPrompt;
|
|
135
29
|
streamOptions;
|
|
30
|
+
compactionSettings;
|
|
136
31
|
getApiKeyAndHeaders;
|
|
137
32
|
resources;
|
|
138
33
|
tools = new Map();
|
|
@@ -143,11 +38,19 @@ export class AgentHarness {
|
|
|
143
38
|
followUpQueueMode;
|
|
144
39
|
nextTurnQueue = [];
|
|
145
40
|
handlers = new Map();
|
|
41
|
+
subscribers = new SubscriberFanout();
|
|
146
42
|
constructor(options) {
|
|
147
43
|
this.env = options.env;
|
|
148
44
|
this.session = options.session;
|
|
45
|
+
this.sessionWrites = new SessionWriteCoordinator(this.session);
|
|
46
|
+
this.lifecycle = new OperationLifecycleController({
|
|
47
|
+
createOperationId: () => uuidv7(),
|
|
48
|
+
now: () => Date.now(),
|
|
49
|
+
});
|
|
50
|
+
this.sessionFacade = new HarnessSessionFacade(this.session, () => this.currentPhase(), this.sessionWrites);
|
|
149
51
|
this.resources = options.resources ?? {};
|
|
150
52
|
this.streamOptions = cloneStreamOptions(options.streamOptions);
|
|
53
|
+
this.compactionSettings = { ...DEFAULT_COMPACTION_SETTINGS, ...options.compaction };
|
|
151
54
|
this.systemPrompt = options.systemPrompt;
|
|
152
55
|
this.getApiKeyAndHeaders = options.getApiKeyAndHeaders;
|
|
153
56
|
this.validateUniqueNames((options.tools ?? []).map((tool) => tool.name), "Duplicate tool name(s)");
|
|
@@ -164,31 +67,19 @@ export class AgentHarness {
|
|
|
164
67
|
this.steeringQueueMode = options.steeringMode ?? "one-at-a-time";
|
|
165
68
|
this.followUpQueueMode = options.followUpMode ?? "one-at-a-time";
|
|
166
69
|
}
|
|
167
|
-
getHandlers(type) {
|
|
168
|
-
return this.handlers.get(type);
|
|
169
|
-
}
|
|
170
70
|
async emitOwn(event, signal) {
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
await listener(event, signal);
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
throw normalizeHookError(error);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
71
|
+
await this.emitAny(event, signal);
|
|
179
72
|
}
|
|
73
|
+
/** Subscriber fan-out; the self-wait barrier lives in `SubscriberFanout`. */
|
|
180
74
|
async emitAny(event, signal) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
throw normalizeHookError(error);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
75
|
+
await this.subscribers.emit(event, this.lifecycle.getCurrentOperation()?.operationId, signal);
|
|
76
|
+
}
|
|
77
|
+
/** Fail closed when an awaited listener tries to wait on its own operation. */
|
|
78
|
+
rejectCurrentOperationSelfWait(api) {
|
|
79
|
+
this.subscribers.assertNotSelfWait(api, this.lifecycle.getCurrentOperation()?.operationId);
|
|
189
80
|
}
|
|
190
81
|
async emitHook(event) {
|
|
191
|
-
const handlers = this.
|
|
82
|
+
const handlers = this.handlers.get(event.type);
|
|
192
83
|
if (!handlers || handlers.size === 0)
|
|
193
84
|
return undefined;
|
|
194
85
|
let lastResult;
|
|
@@ -200,13 +91,13 @@ export class AgentHarness {
|
|
|
200
91
|
}
|
|
201
92
|
}
|
|
202
93
|
catch (error) {
|
|
203
|
-
throw
|
|
94
|
+
throw normalizeHarnessError(error, "hook");
|
|
204
95
|
}
|
|
205
96
|
}
|
|
206
97
|
return lastResult;
|
|
207
98
|
}
|
|
208
99
|
async emitBeforeProviderRequest(model, sessionId, streamOptions) {
|
|
209
|
-
const handlers = this.
|
|
100
|
+
const handlers = this.handlers.get("before_provider_request");
|
|
210
101
|
let current = cloneStreamOptions(streamOptions);
|
|
211
102
|
if (!handlers || handlers.size === 0)
|
|
212
103
|
return current;
|
|
@@ -223,13 +114,13 @@ export class AgentHarness {
|
|
|
223
114
|
}
|
|
224
115
|
}
|
|
225
116
|
catch (error) {
|
|
226
|
-
throw
|
|
117
|
+
throw normalizeHarnessError(error, "hook");
|
|
227
118
|
}
|
|
228
119
|
}
|
|
229
120
|
return current;
|
|
230
121
|
}
|
|
231
122
|
async emitBeforeProviderPayload(model, payload) {
|
|
232
|
-
const handlers = this.
|
|
123
|
+
const handlers = this.handlers.get("before_provider_payload");
|
|
233
124
|
let current = payload;
|
|
234
125
|
if (!handlers || handlers.size === 0)
|
|
235
126
|
return current;
|
|
@@ -241,7 +132,7 @@ export class AgentHarness {
|
|
|
241
132
|
}
|
|
242
133
|
}
|
|
243
134
|
catch (error) {
|
|
244
|
-
throw
|
|
135
|
+
throw normalizeHarnessError(error, "hook");
|
|
245
136
|
}
|
|
246
137
|
}
|
|
247
138
|
return current;
|
|
@@ -254,23 +145,88 @@ export class AgentHarness {
|
|
|
254
145
|
nextTurn: [...this.nextTurnQueue],
|
|
255
146
|
});
|
|
256
147
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Facade write-gate vocabulary mapped from lifecycle state. `settling` maps
|
|
150
|
+
* to "idle": the queue is drained by the settlement finalizer first, and
|
|
151
|
+
* listener writes persist after it through the coordinator tail.
|
|
152
|
+
*/
|
|
153
|
+
currentPhase() {
|
|
154
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
155
|
+
if (snapshot.tag !== "active")
|
|
156
|
+
return "idle";
|
|
157
|
+
switch (snapshot.operation.kind) {
|
|
158
|
+
case "manual_compaction":
|
|
159
|
+
return "compaction";
|
|
160
|
+
case "tree_navigation":
|
|
161
|
+
return "branch_summary";
|
|
162
|
+
default:
|
|
163
|
+
return "turn";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Config writes persist immediately outside an active operation and queue during one. */
|
|
167
|
+
async persistConfigChange(write) {
|
|
168
|
+
if (this.lifecycle.getSnapshot().tag !== "active") {
|
|
169
|
+
await this.sessionWrites.persistAfterPending(write);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.sessionWrites.enqueue(write);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Single wrapper for every public operation: begin a lease, run the body,
|
|
177
|
+
* then settle exactly once. The final flush and the `settled` event happen
|
|
178
|
+
* inside the settling barrier; a finalizer failure never reports success.
|
|
179
|
+
*/
|
|
180
|
+
async runOperation(kind, fallbackCode, body, classifyResult) {
|
|
181
|
+
const lease = this.lifecycle.begin(kind);
|
|
182
|
+
let result;
|
|
183
|
+
let bodyError;
|
|
184
|
+
// Everything after a successful begin() runs inside one capture region. A
|
|
185
|
+
// throwing `operation_started` listener must not escape before settle(),
|
|
186
|
+
// or the lifecycle would stay active and wedge the harness at "busy".
|
|
187
|
+
try {
|
|
188
|
+
await this.emitOwn({ type: "operation_started", operation: lease.operation });
|
|
189
|
+
result = await body(lease);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
bodyError = error;
|
|
193
|
+
}
|
|
194
|
+
// The final flush precedes classification: a persistence failure after a
|
|
195
|
+
// provider success must never record or report a completed operation.
|
|
196
|
+
let flushError;
|
|
197
|
+
try {
|
|
198
|
+
await this.sessionWrites.flush();
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
flushError = error;
|
|
202
|
+
}
|
|
203
|
+
const outcome = resolveOperationOutcome({
|
|
204
|
+
signalAborted: lease.signal.aborted,
|
|
205
|
+
result,
|
|
206
|
+
bodyError,
|
|
207
|
+
flushError,
|
|
208
|
+
classifyResult,
|
|
209
|
+
fallbackCode,
|
|
261
210
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
}
|
|
211
|
+
let settleError;
|
|
212
|
+
try {
|
|
213
|
+
await this.lifecycle.settle(lease, outcome, async () => {
|
|
214
|
+
await this.emitOwn({
|
|
215
|
+
type: "settled",
|
|
216
|
+
nextTurnCount: this.nextTurnQueue.length,
|
|
217
|
+
operationId: lease.operation.operationId,
|
|
218
|
+
outcome,
|
|
219
|
+
attemptCount: this.lifecycle.getAttemptSummaries(lease).length,
|
|
220
|
+
}, lease.signal);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
settleError = error;
|
|
225
|
+
}
|
|
226
|
+
const failure = resolveOperationFailure({ bodyError, flushError, settleError, fallbackCode });
|
|
227
|
+
if (failure !== undefined)
|
|
228
|
+
throw failure;
|
|
229
|
+
return result;
|
|
274
230
|
}
|
|
275
231
|
async createTurnState() {
|
|
276
232
|
const context = await this.session.buildContext();
|
|
@@ -316,13 +272,14 @@ export class AgentHarness {
|
|
|
316
272
|
createStreamFn(getTurnState) {
|
|
317
273
|
return async (model, context, streamOptions) => {
|
|
318
274
|
const turnState = getTurnState();
|
|
275
|
+
const requestContext = await this.maybeAutoCompact(model, context, streamOptions?.signal);
|
|
319
276
|
const auth = await this.getApiKeyAndHeaders?.(model);
|
|
320
277
|
const snapshotOptions = {
|
|
321
278
|
...turnState.streamOptions,
|
|
322
279
|
headers: mergeHeaders(turnState.streamOptions.headers, auth?.headers),
|
|
323
280
|
};
|
|
324
281
|
const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions);
|
|
325
|
-
return streamSimple(model,
|
|
282
|
+
return streamSimple(model, requestContext, {
|
|
326
283
|
cacheRetention: requestOptions.cacheRetention,
|
|
327
284
|
headers: requestOptions.headers,
|
|
328
285
|
maxRetries: requestOptions.maxRetries,
|
|
@@ -352,10 +309,10 @@ export class AgentHarness {
|
|
|
352
309
|
}
|
|
353
310
|
catch (error) {
|
|
354
311
|
queue.unshift(...messages);
|
|
355
|
-
throw
|
|
312
|
+
throw normalizeHarnessError(error, "hook");
|
|
356
313
|
}
|
|
357
314
|
}
|
|
358
|
-
createLoopConfig(getTurnState, setTurnState) {
|
|
315
|
+
createLoopConfig(getTurnState, setTurnState, lease) {
|
|
359
316
|
const turnState = getTurnState();
|
|
360
317
|
return {
|
|
361
318
|
model: turnState.model,
|
|
@@ -389,7 +346,13 @@ export class AgentHarness {
|
|
|
389
346
|
: undefined;
|
|
390
347
|
},
|
|
391
348
|
prepareNextTurn: async () => {
|
|
392
|
-
await this.
|
|
349
|
+
await this.sessionWrites.flush();
|
|
350
|
+
if (lease) {
|
|
351
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
352
|
+
if (snapshot.tag === "active" && snapshot.stage === "save_point") {
|
|
353
|
+
this.lifecycle.setStage(lease, "attempt_running");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
393
356
|
const nextTurnState = await this.createTurnState();
|
|
394
357
|
setTurnState(nextTurnState);
|
|
395
358
|
return {
|
|
@@ -413,40 +376,7 @@ export class AgentHarness {
|
|
|
413
376
|
if (missing.length > 0)
|
|
414
377
|
throw new AgentHarnessError("invalid_argument", `Unknown tool(s): ${missing.join(", ")}`);
|
|
415
378
|
}
|
|
416
|
-
async
|
|
417
|
-
while (this.pendingSessionWrites.length > 0) {
|
|
418
|
-
const write = this.pendingSessionWrites[0];
|
|
419
|
-
if (write.type === "message") {
|
|
420
|
-
await this.session.appendMessage(write.message);
|
|
421
|
-
}
|
|
422
|
-
else if (write.type === "model_change") {
|
|
423
|
-
await this.session.appendModelChange(write.provider, write.modelId);
|
|
424
|
-
}
|
|
425
|
-
else if (write.type === "thinking_level_change") {
|
|
426
|
-
await this.session.appendThinkingLevelChange(write.thinkingLevel);
|
|
427
|
-
}
|
|
428
|
-
else if (write.type === "active_tools_change") {
|
|
429
|
-
await this.session.appendActiveToolsChange(write.activeToolNames);
|
|
430
|
-
}
|
|
431
|
-
else if (write.type === "custom") {
|
|
432
|
-
await this.session.appendCustomEntry(write.customType, write.data);
|
|
433
|
-
}
|
|
434
|
-
else if (write.type === "custom_message") {
|
|
435
|
-
await this.session.appendCustomMessageEntry(write.customType, write.content, write.display, write.details);
|
|
436
|
-
}
|
|
437
|
-
else if (write.type === "label") {
|
|
438
|
-
await this.session.appendLabel(write.targetId, write.label);
|
|
439
|
-
}
|
|
440
|
-
else if (write.type === "session_info") {
|
|
441
|
-
await this.session.appendSessionName(write.name ?? "");
|
|
442
|
-
}
|
|
443
|
-
else if (write.type === "leaf") {
|
|
444
|
-
await this.session.getStorage().setLeafId(write.targetId);
|
|
445
|
-
}
|
|
446
|
-
this.pendingSessionWrites.shift();
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
async handleAgentEvent(event, signal) {
|
|
379
|
+
async handleAgentEvent(event, signal, lease) {
|
|
450
380
|
if (event.type === "message_end") {
|
|
451
381
|
await this.session.appendMessage(event.message);
|
|
452
382
|
await this.emitAny(event, signal);
|
|
@@ -460,39 +390,48 @@ export class AgentHarness {
|
|
|
460
390
|
catch (error) {
|
|
461
391
|
eventError = error;
|
|
462
392
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
393
|
+
// The flush runs even after a failing listener so accepted writes are
|
|
394
|
+
// not stranded; a failing flush must then report next to that listener
|
|
395
|
+
// error, not in place of it.
|
|
396
|
+
const hadPendingMutations = this.sessionWrites.hasPending();
|
|
397
|
+
let flushError;
|
|
398
|
+
try {
|
|
399
|
+
await this.sessionWrites.flush();
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
flushError = error;
|
|
403
|
+
}
|
|
404
|
+
if (lease) {
|
|
405
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
406
|
+
if (snapshot.tag === "active" && snapshot.stage === "attempt_running") {
|
|
407
|
+
this.lifecycle.setStage(lease, "save_point");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const failure = combineBoundaryErrors([eventError, flushError], "turn_end listener failed and the save-point flush failed", "hook");
|
|
411
|
+
if (failure !== undefined)
|
|
412
|
+
throw failure;
|
|
467
413
|
await this.emitOwn({ type: "save_point", hadPendingMutations });
|
|
468
414
|
return;
|
|
469
415
|
}
|
|
470
416
|
if (event.type === "agent_end") {
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
await this.flushPendingSessionWrites();
|
|
475
|
-
if (this.runAbortController && this.runAbortController.signal === signal) {
|
|
476
|
-
this.runAbortController = undefined;
|
|
477
|
-
}
|
|
478
|
-
this.phase = "idle";
|
|
417
|
+
// agent_end is an attempt event: flush its accepted writes, but lifecycle
|
|
418
|
+
// settlement and the settled event belong to OperationLease.settle().
|
|
419
|
+
await this.sessionWrites.flush();
|
|
479
420
|
await this.emitAny(event, signal);
|
|
480
|
-
await this.emitOwn({ type: "settled", nextTurnCount: this.nextTurnQueue.length }, signal);
|
|
481
421
|
return;
|
|
482
422
|
}
|
|
483
423
|
await this.emitAny(event, signal);
|
|
484
424
|
}
|
|
485
|
-
async emitRunFailure(model, error, aborted, signal, completedMessages) {
|
|
425
|
+
async emitRunFailure(model, error, aborted, signal, completedMessages, lease) {
|
|
486
426
|
const failureMessage = createFailureMessage(model, error, aborted);
|
|
487
427
|
const messages = [...completedMessages, failureMessage];
|
|
488
|
-
await this.handleAgentEvent({ type: "message_start", message: failureMessage }, signal);
|
|
489
|
-
await this.handleAgentEvent({ type: "message_end", message: failureMessage }, signal);
|
|
490
|
-
await this.handleAgentEvent({ type: "turn_end", message: failureMessage, toolResults: [] }, signal);
|
|
491
|
-
await this.handleAgentEvent({ type: "agent_end", messages }, signal);
|
|
428
|
+
await this.handleAgentEvent({ type: "message_start", message: failureMessage }, signal, lease);
|
|
429
|
+
await this.handleAgentEvent({ type: "message_end", message: failureMessage }, signal, lease);
|
|
430
|
+
await this.handleAgentEvent({ type: "turn_end", message: failureMessage, toolResults: [] }, signal, lease);
|
|
431
|
+
await this.handleAgentEvent({ type: "agent_end", messages }, signal, lease);
|
|
492
432
|
return messages;
|
|
493
433
|
}
|
|
494
|
-
async executeTurn(turnState, text, options) {
|
|
495
|
-
let activeTurnState = turnState;
|
|
434
|
+
async executeTurn(lease, turnState, text, options) {
|
|
496
435
|
let messages = [createUserMessage(text, options?.images)];
|
|
497
436
|
if (this.nextTurnQueue.length > 0) {
|
|
498
437
|
const queuedMessages = this.nextTurnQueue.splice(0);
|
|
@@ -501,7 +440,7 @@ export class AgentHarness {
|
|
|
501
440
|
}
|
|
502
441
|
catch (error) {
|
|
503
442
|
this.nextTurnQueue.unshift(...queuedMessages);
|
|
504
|
-
throw
|
|
443
|
+
throw normalizeHarnessError(error, "hook");
|
|
505
444
|
}
|
|
506
445
|
messages = [...queuedMessages, messages[0]];
|
|
507
446
|
}
|
|
@@ -514,125 +453,173 @@ export class AgentHarness {
|
|
|
514
453
|
});
|
|
515
454
|
if (beforeResult?.messages)
|
|
516
455
|
messages = [...messages, ...beforeResult.messages];
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
456
|
+
const result = await this.executeAgentRun(lease, "initial", turnState, this.createContext(turnState, beforeResult?.systemPrompt), messages);
|
|
457
|
+
return await this.recoverContextOverflow(lease, result);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Sole attempt boundary: begin, announce, run, classify, close, announce, flush.
|
|
461
|
+
*
|
|
462
|
+
* Once `beginAttempt()` succeeds the attempt is closed exactly once on every
|
|
463
|
+
* path, so `count(attempt_started) == count(attempt_finished)` holds even when
|
|
464
|
+
* the `attempt_started` observer throws. `attempt_finished` is emitted only
|
|
465
|
+
* after the attempt is already closed, so a throwing observer can fail the
|
|
466
|
+
* operation but can never reopen committed attempt state. The closing flush
|
|
467
|
+
* is not a `finally`: a `finally` that awaits a throwing flush would replace
|
|
468
|
+
* the body error, hiding the provider or listener failure from the audit trail.
|
|
469
|
+
*/
|
|
470
|
+
async runAttempt(lease, reason, body, classify) {
|
|
471
|
+
const attemptLease = this.lifecycle.beginAttempt(lease, reason);
|
|
472
|
+
let result;
|
|
473
|
+
let bodyError;
|
|
474
|
+
try {
|
|
475
|
+
await this.emitOwn({ type: "attempt_started", attempt: attemptLease.attempt }, lease.signal);
|
|
476
|
+
result = await body(attemptLease);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
bodyError = error;
|
|
480
|
+
}
|
|
481
|
+
const outcome = bodyError === undefined ? classify(result) : classifyAttemptFailure(bodyError);
|
|
482
|
+
this.lifecycle.finishAttempt(lease, attemptLease, outcome);
|
|
483
|
+
let observerError;
|
|
484
|
+
try {
|
|
485
|
+
await this.emitOwn({ type: "attempt_finished", summary: this.lifecycle.getAttemptSummary(lease, attemptLease) }, lease.signal);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
observerError = error;
|
|
489
|
+
}
|
|
490
|
+
let flushError;
|
|
491
|
+
try {
|
|
492
|
+
await this.sessionWrites.flush();
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
flushError = error;
|
|
496
|
+
}
|
|
497
|
+
const failure = combineBoundaryErrors([bodyError, observerError, flushError], "Attempt failed and its attempt_finished reporting or closing flush failed", "unknown");
|
|
498
|
+
if (failure !== undefined)
|
|
499
|
+
throw failure;
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
async executeAgentRun(lease, reason, turnState, context, initialMessages) {
|
|
503
|
+
let activeTurnState = turnState;
|
|
504
|
+
return await this.runAttempt(lease, reason, async (attemptLease) => {
|
|
505
|
+
const signal = attemptLease.signal;
|
|
506
|
+
const getTurnState = () => activeTurnState;
|
|
507
|
+
const setTurnState = (nextTurnState) => {
|
|
508
|
+
activeTurnState = nextTurnState;
|
|
509
|
+
};
|
|
510
|
+
const completedMessages = [];
|
|
511
|
+
const emit = async (event) => {
|
|
512
|
+
if (event.type === "message_end")
|
|
513
|
+
completedMessages.push(event.message);
|
|
514
|
+
await this.handleAgentEvent(event, signal, lease);
|
|
515
|
+
};
|
|
516
|
+
let newMessages;
|
|
525
517
|
try {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
await
|
|
530
|
-
|
|
518
|
+
const loopConfig = this.createLoopConfig(getTurnState, setTurnState, lease);
|
|
519
|
+
const streamFn = this.createStreamFn(getTurnState);
|
|
520
|
+
newMessages = initialMessages
|
|
521
|
+
? await runAgentLoop(initialMessages, context, loopConfig, emit, signal, streamFn)
|
|
522
|
+
: await runAgentLoopContinue(context, loopConfig, emit, signal, streamFn);
|
|
531
523
|
}
|
|
532
524
|
catch (error) {
|
|
533
525
|
try {
|
|
534
|
-
|
|
526
|
+
newMessages = await this.emitRunFailure(activeTurnState.model, error, signal.aborted, signal, completedMessages, lease);
|
|
535
527
|
}
|
|
536
528
|
catch (failureError) {
|
|
537
529
|
const cause = new AggregateError([toError(error), toError(failureError)], "Agent run failed and failure reporting failed");
|
|
538
530
|
throw new AgentHarnessError("unknown", cause.message, cause);
|
|
539
531
|
}
|
|
540
532
|
}
|
|
541
|
-
})();
|
|
542
|
-
try {
|
|
543
|
-
const newMessages = await runResultPromise;
|
|
544
533
|
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
545
534
|
const message = newMessages[i];
|
|
546
|
-
if (message.role === "assistant")
|
|
535
|
+
if (message.role === "assistant")
|
|
547
536
|
return message;
|
|
548
|
-
}
|
|
549
537
|
}
|
|
550
538
|
throw new AgentHarnessError("invalid_state", "AgentHarness prompt completed without an assistant message");
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
539
|
+
}, (message) => classifyAttemptOutcome(message, activeTurnState.model.contextWindow));
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* One-shot overflow recovery inside the originating operation. The lease is
|
|
543
|
+
* proof that this operation still owns the harness, so no run-ownership or
|
|
544
|
+
* phase re-check is needed; a strict lifecycle makes a newer operation
|
|
545
|
+
* starting mid-recovery impossible.
|
|
546
|
+
*/
|
|
547
|
+
async recoverContextOverflow(lease, message) {
|
|
548
|
+
if (!this.compactionSettings.enabled || !isContextOverflow(message, this.model.contextWindow)) {
|
|
549
|
+
return message;
|
|
550
|
+
}
|
|
551
|
+
if (!this.getApiKeyAndHeaders)
|
|
552
|
+
return message;
|
|
553
|
+
const leafId = await this.session.getLeafId();
|
|
554
|
+
if (!leafId)
|
|
555
|
+
return message;
|
|
556
|
+
const leaf = await this.session.getEntry(leafId);
|
|
557
|
+
if (leaf?.type !== "message" ||
|
|
558
|
+
leaf.message.role !== "assistant" ||
|
|
559
|
+
leaf.message.timestamp !== message.timestamp ||
|
|
560
|
+
!isContextOverflow(leaf.message, this.model.contextWindow)) {
|
|
561
|
+
return message;
|
|
562
|
+
}
|
|
563
|
+
await this.session.moveTo(leaf.parentId);
|
|
564
|
+
this.lifecycle.setStage(lease, "recovering_overflow");
|
|
570
565
|
try {
|
|
566
|
+
const compacted = await this.runCompaction({ automatic: true, signal: lease.signal });
|
|
567
|
+
if (!compacted) {
|
|
568
|
+
await this.session.moveTo(leafId);
|
|
569
|
+
return message;
|
|
570
|
+
}
|
|
571
571
|
const turnState = await this.createTurnState();
|
|
572
|
-
return await this.
|
|
572
|
+
return await this.executeAgentRun(lease, "context_overflow_recovery", turnState, this.createContext(turnState));
|
|
573
573
|
}
|
|
574
574
|
catch (error) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (this.runPromise === runPromise)
|
|
578
|
-
this.phase = "idle";
|
|
579
|
-
throw normalizeHarnessError(error, "unknown");
|
|
580
|
-
}
|
|
581
|
-
finally {
|
|
582
|
-
finishRunPromise();
|
|
575
|
+
await this.session.moveTo(leafId);
|
|
576
|
+
throw error;
|
|
583
577
|
}
|
|
584
578
|
}
|
|
579
|
+
async prompt(text, options) {
|
|
580
|
+
return this.runOperation("prompt", "unknown", async (lease) => {
|
|
581
|
+
const turnState = await this.createTurnState();
|
|
582
|
+
return await this.executeTurn(lease, turnState, text, options);
|
|
583
|
+
}, classifyAssistantOutcome);
|
|
584
|
+
}
|
|
585
585
|
async skill(name, additionalInstructions) {
|
|
586
|
-
|
|
587
|
-
throw new AgentHarnessError("busy", "AgentHarness is busy");
|
|
588
|
-
this.phase = "turn";
|
|
589
|
-
const { runPromise, finishRunPromise } = this.startRunPromise();
|
|
590
|
-
try {
|
|
586
|
+
return this.runOperation("skill", "unknown", async (lease) => {
|
|
591
587
|
const turnState = await this.createTurnState();
|
|
592
588
|
const skill = (turnState.resources.skills ?? []).find((candidate) => candidate.name === name);
|
|
593
589
|
if (!skill)
|
|
594
590
|
throw new AgentHarnessError("invalid_argument", `Unknown skill: ${name}`);
|
|
595
|
-
return await this.executeTurn(turnState, formatSkillInvocation(skill, additionalInstructions));
|
|
596
|
-
}
|
|
597
|
-
catch (error) {
|
|
598
|
-
if (this.runPromise === runPromise)
|
|
599
|
-
this.phase = "idle";
|
|
600
|
-
throw normalizeHarnessError(error, "unknown");
|
|
601
|
-
}
|
|
602
|
-
finally {
|
|
603
|
-
finishRunPromise();
|
|
604
|
-
}
|
|
591
|
+
return await this.executeTurn(lease, turnState, formatSkillInvocation(skill, additionalInstructions));
|
|
592
|
+
}, classifyAssistantOutcome);
|
|
605
593
|
}
|
|
606
594
|
async promptFromTemplate(name, args = []) {
|
|
607
|
-
|
|
608
|
-
throw new AgentHarnessError("busy", "AgentHarness is busy");
|
|
609
|
-
this.phase = "turn";
|
|
610
|
-
const { runPromise, finishRunPromise } = this.startRunPromise();
|
|
611
|
-
try {
|
|
595
|
+
return this.runOperation("prompt_template", "unknown", async (lease) => {
|
|
612
596
|
const turnState = await this.createTurnState();
|
|
613
597
|
const template = (turnState.resources.promptTemplates ?? []).find((candidate) => candidate.name === name);
|
|
614
598
|
if (!template)
|
|
615
599
|
throw new AgentHarnessError("invalid_argument", `Unknown prompt template: ${name}`);
|
|
616
|
-
return await this.executeTurn(turnState, formatPromptTemplateInvocation(template, args));
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
600
|
+
return await this.executeTurn(lease, turnState, formatPromptTemplateInvocation(template, args));
|
|
601
|
+
}, classifyAssistantOutcome);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Steering and follow-up input is consumed only by a running agent attempt.
|
|
605
|
+
* A structural operation (`compact`, `navigateTree`) runs none, so accepting
|
|
606
|
+
* input there would silently inject it into an unrelated later prompt.
|
|
607
|
+
*/
|
|
608
|
+
expectQueueConsumer(action) {
|
|
609
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
610
|
+
if (snapshot.tag !== "active")
|
|
611
|
+
throw new AgentHarnessError("invalid_state", `Cannot ${action} while idle`);
|
|
612
|
+
if (!PROMPT_FAMILY_KINDS.includes(snapshot.operation.kind)) {
|
|
613
|
+
throw new AgentHarnessError("invalid_state", `Cannot ${action} during ${snapshot.operation.kind}: no agent attempt can consume it`);
|
|
625
614
|
}
|
|
626
615
|
}
|
|
627
616
|
async steer(text, options) {
|
|
628
|
-
|
|
629
|
-
throw new AgentHarnessError("invalid_state", "Cannot steer while idle");
|
|
617
|
+
this.expectQueueConsumer("steer");
|
|
630
618
|
this.steerQueue.push(createUserMessage(text, options?.images));
|
|
631
619
|
await this.emitQueueUpdate();
|
|
632
620
|
}
|
|
633
621
|
async followUp(text, options) {
|
|
634
|
-
|
|
635
|
-
throw new AgentHarnessError("invalid_state", "Cannot follow up while idle");
|
|
622
|
+
this.expectQueueConsumer("follow up");
|
|
636
623
|
this.followUpQueue.push(createUserMessage(text, options?.images));
|
|
637
624
|
await this.emitQueueUpdate();
|
|
638
625
|
}
|
|
@@ -640,73 +627,89 @@ export class AgentHarness {
|
|
|
640
627
|
this.nextTurnQueue.push(createUserMessage(text, options?.images));
|
|
641
628
|
await this.emitQueueUpdate();
|
|
642
629
|
}
|
|
630
|
+
getSession() {
|
|
631
|
+
return this.sessionFacade;
|
|
632
|
+
}
|
|
643
633
|
async appendMessage(message) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
634
|
+
await this.sessionFacade.appendMessage(message);
|
|
635
|
+
}
|
|
636
|
+
async runCompaction(options) {
|
|
637
|
+
const model = this.model;
|
|
638
|
+
const auth = await this.getApiKeyAndHeaders?.(model);
|
|
639
|
+
if (!auth) {
|
|
640
|
+
if (options.automatic)
|
|
641
|
+
return undefined;
|
|
642
|
+
throw new AgentHarnessError("auth", "No auth available for compaction");
|
|
643
|
+
}
|
|
644
|
+
const branchEntries = await this.session.getBranch();
|
|
645
|
+
const preparationResult = prepareCompaction(branchEntries, this.compactionSettings);
|
|
646
|
+
if (!preparationResult.ok)
|
|
647
|
+
throw preparationResult.error;
|
|
648
|
+
const preparation = preparationResult.value;
|
|
649
|
+
if (!preparation) {
|
|
650
|
+
if (options.automatic)
|
|
651
|
+
return undefined;
|
|
652
|
+
throw new AgentHarnessError("compaction", "Nothing to compact");
|
|
653
|
+
}
|
|
654
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
655
|
+
const hookResult = await this.emitHook({
|
|
656
|
+
type: "session_before_compact",
|
|
657
|
+
preparation,
|
|
658
|
+
branchEntries,
|
|
659
|
+
customInstructions: options.customInstructions,
|
|
660
|
+
signal,
|
|
661
|
+
});
|
|
662
|
+
if (hookResult?.cancel) {
|
|
663
|
+
if (options.automatic)
|
|
664
|
+
return undefined;
|
|
665
|
+
throw new AgentHarnessError("compaction", "Compaction cancelled");
|
|
666
|
+
}
|
|
667
|
+
const provided = hookResult?.compaction;
|
|
668
|
+
const compactResult = provided
|
|
669
|
+
? { ok: true, value: provided }
|
|
670
|
+
: await compact(preparation, model, auth.apiKey, auth.headers, options.customInstructions, signal, this.thinkingLevel, createSummarizationRetry("compaction", this.streamOptions.summarizationRetry, (event) => this.emitOwn(event)));
|
|
671
|
+
if (!compactResult.ok)
|
|
672
|
+
throw compactResult.error;
|
|
673
|
+
const result = compactResult.value;
|
|
674
|
+
options.beforeCommit?.();
|
|
675
|
+
const entryId = await this.session.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, provided !== undefined);
|
|
676
|
+
const entry = await this.session.getEntry(entryId);
|
|
677
|
+
if (entry?.type === "compaction") {
|
|
678
|
+
await this.emitOwn({ type: "session_compact", compactionEntry: entry, fromHook: provided !== undefined });
|
|
654
679
|
}
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
async maybeAutoCompact(model, context, signal) {
|
|
683
|
+
const projected = estimateContextTokens(context.messages).tokens;
|
|
684
|
+
if (!shouldCompact(projected, model.contextWindow ?? 0, this.compactionSettings))
|
|
685
|
+
return context;
|
|
686
|
+
const result = await this.runCompaction({ automatic: true, signal });
|
|
687
|
+
if (!result)
|
|
688
|
+
return context;
|
|
689
|
+
const persisted = await this.session.buildContext();
|
|
690
|
+
return { ...context, messages: convertToLlm(persisted.messages) };
|
|
655
691
|
}
|
|
656
692
|
async compact(customInstructions) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const model = this.model;
|
|
662
|
-
if (!model)
|
|
663
|
-
throw new AgentHarnessError("invalid_state", "No model set for compaction");
|
|
664
|
-
const auth = await this.getApiKeyAndHeaders?.(model);
|
|
665
|
-
if (!auth)
|
|
666
|
-
throw new AgentHarnessError("auth", "No auth available for compaction");
|
|
667
|
-
const branchEntries = await this.session.getBranch();
|
|
668
|
-
const preparationResult = prepareCompaction(branchEntries, DEFAULT_COMPACTION_SETTINGS);
|
|
669
|
-
if (!preparationResult.ok)
|
|
670
|
-
throw preparationResult.error;
|
|
671
|
-
const preparation = preparationResult.value;
|
|
672
|
-
if (!preparation)
|
|
673
|
-
throw new AgentHarnessError("compaction", "Nothing to compact");
|
|
674
|
-
const hookResult = await this.emitHook({
|
|
675
|
-
type: "session_before_compact",
|
|
676
|
-
preparation,
|
|
677
|
-
branchEntries,
|
|
693
|
+
return this.runOperation("manual_compaction", "compaction", async (lease) => {
|
|
694
|
+
this.lifecycle.setStage(lease, "structural_running");
|
|
695
|
+
const result = await this.runCompaction({
|
|
696
|
+
automatic: false,
|
|
678
697
|
customInstructions,
|
|
679
|
-
|
|
698
|
+
beforeCommit: () => {
|
|
699
|
+
this.lifecycle.setStage(lease, "committing");
|
|
700
|
+
},
|
|
680
701
|
});
|
|
681
|
-
if (
|
|
682
|
-
throw new AgentHarnessError("compaction", "
|
|
683
|
-
const provided = hookResult?.compaction;
|
|
684
|
-
const compactResult = provided
|
|
685
|
-
? { ok: true, value: provided }
|
|
686
|
-
: await compact(preparation, model, auth.apiKey, auth.headers, customInstructions, undefined, this.thinkingLevel);
|
|
687
|
-
if (!compactResult.ok)
|
|
688
|
-
throw compactResult.error;
|
|
689
|
-
const result = compactResult.value;
|
|
690
|
-
const entryId = await this.session.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, provided !== undefined);
|
|
691
|
-
const entry = await this.session.getEntry(entryId);
|
|
692
|
-
if (entry?.type === "compaction") {
|
|
693
|
-
await this.emitOwn({ type: "session_compact", compactionEntry: entry, fromHook: provided !== undefined });
|
|
694
|
-
}
|
|
702
|
+
if (!result)
|
|
703
|
+
throw new AgentHarnessError("compaction", "Nothing to compact");
|
|
695
704
|
return result;
|
|
696
|
-
}
|
|
697
|
-
catch (error) {
|
|
698
|
-
throw normalizeHarnessError(error, "compaction");
|
|
699
|
-
}
|
|
700
|
-
finally {
|
|
701
|
-
this.phase = "idle";
|
|
702
|
-
}
|
|
705
|
+
});
|
|
703
706
|
}
|
|
704
707
|
async navigateTree(targetId, options) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
this.phase = "branch_summary";
|
|
708
|
-
try {
|
|
708
|
+
return this.runOperation("tree_navigation", "branch_summary", async (lease) => {
|
|
709
|
+
this.lifecycle.setStage(lease, "structural_running");
|
|
709
710
|
const oldLeafId = await this.session.getLeafId();
|
|
711
|
+
// No-op navigation mutates nothing, so it completes without ever
|
|
712
|
+
// entering the `committing` stage.
|
|
710
713
|
if (oldLeafId === targetId)
|
|
711
714
|
return { cancelled: false };
|
|
712
715
|
const targetEntry = await this.session.getEntry(targetId);
|
|
@@ -737,51 +740,24 @@ export class AgentHarness {
|
|
|
737
740
|
const auth = await this.getApiKeyAndHeaders?.(model);
|
|
738
741
|
if (!auth)
|
|
739
742
|
throw new AgentHarnessError("auth", "No auth available for branch summary");
|
|
740
|
-
const branchSummary = await
|
|
743
|
+
const branchSummary = await runBranchSummary({
|
|
744
|
+
entries,
|
|
741
745
|
model,
|
|
742
746
|
apiKey: auth.apiKey,
|
|
743
747
|
headers: auth.headers,
|
|
744
|
-
signal: new AbortController().signal,
|
|
745
748
|
customInstructions: hookResult?.customInstructions ?? options?.customInstructions,
|
|
746
749
|
replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions,
|
|
750
|
+
summarizationRetry: this.streamOptions.summarizationRetry,
|
|
751
|
+
emit: (event) => this.emitOwn(event),
|
|
747
752
|
});
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
modifiedFiles: branchSummary.value.modifiedFiles,
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
let editorText;
|
|
760
|
-
let newLeafId;
|
|
761
|
-
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
762
|
-
newLeafId = targetEntry.parentId;
|
|
763
|
-
const content = targetEntry.message.content;
|
|
764
|
-
editorText =
|
|
765
|
-
typeof content === "string"
|
|
766
|
-
? content
|
|
767
|
-
: content
|
|
768
|
-
.filter((c) => c.type === "text")
|
|
769
|
-
.map((c) => c.text)
|
|
770
|
-
.join("");
|
|
771
|
-
}
|
|
772
|
-
else if (targetEntry.type === "custom_message") {
|
|
773
|
-
newLeafId = targetEntry.parentId;
|
|
774
|
-
editorText =
|
|
775
|
-
typeof targetEntry.content === "string"
|
|
776
|
-
? targetEntry.content
|
|
777
|
-
: targetEntry.content
|
|
778
|
-
.filter((c) => c.type === "text")
|
|
779
|
-
.map((c) => c.text)
|
|
780
|
-
.join("");
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
newLeafId = targetId;
|
|
784
|
-
}
|
|
753
|
+
if (branchSummary.cancelled)
|
|
754
|
+
return { cancelled: true };
|
|
755
|
+
summaryText = branchSummary.summary;
|
|
756
|
+
summaryDetails = branchSummary.details;
|
|
757
|
+
}
|
|
758
|
+
const { newLeafId, editorText } = resolveNavigationTarget(targetEntry, targetId);
|
|
759
|
+
// Single declared commit point of a tree navigation.
|
|
760
|
+
this.lifecycle.setStage(lease, "committing");
|
|
785
761
|
const summaryId = await this.session.moveTo(newLeafId, summaryText
|
|
786
762
|
? { summary: summaryText, details: summaryDetails, fromHook: hookResult?.summary !== undefined }
|
|
787
763
|
: undefined);
|
|
@@ -798,13 +774,7 @@ export class AgentHarness {
|
|
|
798
774
|
fromHook: hookResult?.summary !== undefined,
|
|
799
775
|
});
|
|
800
776
|
return { cancelled: false, editorText, summaryEntry };
|
|
801
|
-
}
|
|
802
|
-
catch (error) {
|
|
803
|
-
throw normalizeHarnessError(error, "branch_summary");
|
|
804
|
-
}
|
|
805
|
-
finally {
|
|
806
|
-
this.phase = "idle";
|
|
807
|
-
}
|
|
777
|
+
}, classifyNavigateTreeOutcome);
|
|
808
778
|
}
|
|
809
779
|
getModel() {
|
|
810
780
|
return this.model;
|
|
@@ -812,12 +782,9 @@ export class AgentHarness {
|
|
|
812
782
|
async setModel(model) {
|
|
813
783
|
try {
|
|
814
784
|
const previousModel = this.model;
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
else {
|
|
819
|
-
this.pendingSessionWrites.push({ type: "model_change", provider: model.provider, modelId: model.id });
|
|
820
|
-
}
|
|
785
|
+
const nextProvider = model.provider;
|
|
786
|
+
const nextModelId = model.id;
|
|
787
|
+
await this.persistConfigChange({ type: "model_change", provider: nextProvider, modelId: nextModelId });
|
|
821
788
|
this.model = model;
|
|
822
789
|
await this.emitOwn({ type: "model_update", model, previousModel, source: "set" });
|
|
823
790
|
}
|
|
@@ -831,12 +798,7 @@ export class AgentHarness {
|
|
|
831
798
|
async setThinkingLevel(level) {
|
|
832
799
|
try {
|
|
833
800
|
const previousLevel = this.thinkingLevel;
|
|
834
|
-
|
|
835
|
-
await this.session.appendThinkingLevelChange(level);
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
this.pendingSessionWrites.push({ type: "thinking_level_change", thinkingLevel: level });
|
|
839
|
-
}
|
|
801
|
+
await this.persistConfigChange({ type: "thinking_level_change", thinkingLevel: level });
|
|
840
802
|
this.thinkingLevel = level;
|
|
841
803
|
await this.emitOwn({ type: "thinking_level_update", level, previousLevel });
|
|
842
804
|
}
|
|
@@ -855,12 +817,7 @@ export class AgentHarness {
|
|
|
855
817
|
this.validateToolNames(nextActiveToolNames, nextTools);
|
|
856
818
|
const previousToolNames = [...this.tools.keys()];
|
|
857
819
|
const previousActiveToolNames = [...this.activeToolNames];
|
|
858
|
-
|
|
859
|
-
await this.session.appendActiveToolsChange(nextActiveToolNames);
|
|
860
|
-
}
|
|
861
|
-
else {
|
|
862
|
-
this.pendingSessionWrites.push({ type: "active_tools_change", activeToolNames: [...nextActiveToolNames] });
|
|
863
|
-
}
|
|
820
|
+
await this.persistConfigChange({ type: "active_tools_change", activeToolNames: [...nextActiveToolNames] });
|
|
864
821
|
this.tools = nextTools;
|
|
865
822
|
this.activeToolNames = [...nextActiveToolNames];
|
|
866
823
|
await this.emitOwn({
|
|
@@ -881,16 +838,12 @@ export class AgentHarness {
|
|
|
881
838
|
}
|
|
882
839
|
async setActiveTools(toolNames) {
|
|
883
840
|
try {
|
|
884
|
-
|
|
841
|
+
const nextActiveToolNames = [...toolNames];
|
|
842
|
+
this.validateToolNames(nextActiveToolNames);
|
|
885
843
|
const previousToolNames = [...this.tools.keys()];
|
|
886
844
|
const previousActiveToolNames = [...this.activeToolNames];
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
else {
|
|
891
|
-
this.pendingSessionWrites.push({ type: "active_tools_change", activeToolNames: [...toolNames] });
|
|
892
|
-
}
|
|
893
|
-
this.activeToolNames = [...toolNames];
|
|
845
|
+
await this.persistConfigChange({ type: "active_tools_change", activeToolNames: [...nextActiveToolNames] });
|
|
846
|
+
this.activeToolNames = [...nextActiveToolNames];
|
|
894
847
|
await this.emitOwn({
|
|
895
848
|
type: "tools_update",
|
|
896
849
|
toolNames: [...this.tools.keys()],
|
|
@@ -937,11 +890,21 @@ export class AgentHarness {
|
|
|
937
890
|
this.streamOptions = cloneStreamOptions(streamOptions);
|
|
938
891
|
}
|
|
939
892
|
async abort() {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
this.
|
|
943
|
-
|
|
944
|
-
|
|
893
|
+
// Aborting awaits the captured operation's settlement, so a listener of that
|
|
894
|
+
// same operation must never reach the wait below.
|
|
895
|
+
this.rejectCurrentOperationSelfWait("abort()");
|
|
896
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
897
|
+
if (snapshot.tag === "active" && snapshot.operation.kind === "manual_compaction") {
|
|
898
|
+
throw new AgentHarnessError("invalid_state", "Cannot abort during compaction");
|
|
899
|
+
}
|
|
900
|
+
if (snapshot.tag === "active" && snapshot.operation.kind === "tree_navigation") {
|
|
901
|
+
throw new AgentHarnessError("invalid_state", "Cannot abort during branch_summary");
|
|
902
|
+
}
|
|
903
|
+
// Capture the current operation before delivering any signal: an operation
|
|
904
|
+
// started later by a settlement listener is never this call's target.
|
|
905
|
+
const capture = this.lifecycle.requestAbort();
|
|
906
|
+
const clearedSteer = this.steerQueue.splice(0);
|
|
907
|
+
const clearedFollowUp = this.followUpQueue.splice(0);
|
|
945
908
|
const errors = [];
|
|
946
909
|
try {
|
|
947
910
|
await this.emitQueueUpdate();
|
|
@@ -950,7 +913,8 @@ export class AgentHarness {
|
|
|
950
913
|
errors.push(toError(error));
|
|
951
914
|
}
|
|
952
915
|
try {
|
|
953
|
-
|
|
916
|
+
if (capture.target)
|
|
917
|
+
await capture.target.settled;
|
|
954
918
|
}
|
|
955
919
|
catch (error) {
|
|
956
920
|
errors.push(toError(error));
|
|
@@ -968,20 +932,12 @@ export class AgentHarness {
|
|
|
968
932
|
return { clearedSteer, clearedFollowUp };
|
|
969
933
|
}
|
|
970
934
|
async waitForIdle() {
|
|
971
|
-
|
|
972
|
-
//
|
|
973
|
-
|
|
974
|
-
await this.runPromise;
|
|
975
|
-
}
|
|
935
|
+
this.rejectCurrentOperationSelfWait("waitForIdle()");
|
|
936
|
+
// Delegates to the lifecycle: resolves once no operation is active or settling.
|
|
937
|
+
await this.lifecycle.waitForIdle();
|
|
976
938
|
}
|
|
977
939
|
subscribe(listener) {
|
|
978
|
-
|
|
979
|
-
if (!handlers) {
|
|
980
|
-
handlers = new Set();
|
|
981
|
-
this.handlers.set(SUBSCRIBER_EVENT_TYPE, handlers);
|
|
982
|
-
}
|
|
983
|
-
handlers.add(listener);
|
|
984
|
-
return () => handlers.delete(listener);
|
|
940
|
+
return this.subscribers.subscribe(listener);
|
|
985
941
|
}
|
|
986
942
|
on(type, handler) {
|
|
987
943
|
let handlers = this.handlers.get(type);
|