tinker-agent 2.8.0 → 2.10.0
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 +79 -1
- package/README.md +81 -11
- package/package.json +5 -3
- package/src/agent/runtime-context-capabilities.ts +19 -0
- package/src/agent/runtime-context-events.ts +127 -0
- package/src/agent/runtime-context-maintenance.ts +780 -0
- package/src/agent/runtime-hosted-session.ts +443 -0
- package/src/agent/runtime-interactions.ts +291 -0
- package/src/agent/runtime-prompt-scheduler.ts +182 -0
- package/src/agent/runtime-session-contracts.ts +317 -0
- package/src/agent/runtime-session.ts +250 -2130
- package/src/agent/runtime-skills.ts +544 -0
- package/src/cli/command-line.ts +26 -2
- package/src/cli/connect-runner.tsx +26 -0
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +1 -1
- package/src/cli/public-cli-contract.ts +18 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/runner-dependencies.ts +6 -5
- package/src/cli/serve-runner.ts +45 -0
- package/src/cli/serve-runtime.ts +100 -0
- package/src/context/context-automation-policy.ts +12 -118
- package/src/context/context-swap-renderer.ts +14 -0
- package/src/events/types.ts +12 -0
- package/src/memory/memory-get-tool.ts +1 -1
- package/src/observation/observation-builder.ts +128 -48
- package/src/remote/client.ts +350 -0
- package/src/remote/config.ts +95 -0
- package/src/remote/http-server.ts +240 -0
- package/src/remote/protocol.ts +228 -0
- package/src/remote/service-store.ts +175 -0
- package/src/remote/service.ts +219 -0
- package/src/remote/sync-hub.ts +95 -0
- package/src/session/remote-history-reader.ts +143 -0
- package/src/session/resume-projection.ts +47 -21
- package/src/session/session-history-access.ts +238 -0
- package/src/session/session-store-context-readers.ts +183 -0
- package/src/session/session-store-ledger-writer.ts +315 -0
- package/src/session/session-store-record-writer.ts +318 -0
- package/src/session/session-store-recovery.ts +225 -0
- package/src/session/session-store-revisions.ts +1004 -0
- package/src/session/session-store-sql.ts +40 -0
- package/src/session/session-store-validation.ts +657 -0
- package/src/session/session-store.ts +756 -3186
- package/src/tools/bash-task.ts +46 -18
- package/src/tools/bash.ts +44 -2
- package/src/tools/glob.ts +107 -19
- package/src/tools/grep-output.ts +130 -0
- package/src/tools/grep-pagination.ts +73 -0
- package/src/tools/grep-path.ts +11 -0
- package/src/tools/grep-snippets.ts +111 -0
- package/src/tools/grep.ts +139 -154
- package/src/tools/read.ts +0 -9
- package/src/tools/recall.ts +106 -50
- package/src/tools/registry.ts +4 -6
- package/src/tools/ripgrep.ts +19 -26
- package/src/tools/shell-process.ts +30 -4
- package/src/tools/task-output-range.ts +146 -0
- package/src/tools/task-output-tool.ts +35 -5
- package/src/tools/task-output.ts +35 -0
- package/src/tools/task-stop.ts +2 -1
- package/src/tools/task-tool-args.ts +34 -0
- package/src/tools/terminal-screen.ts +11 -2
- package/src/tools/types.ts +39 -2
- package/src/tui/event-store.ts +23 -5
- package/src/tui/remote-app.tsx +210 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type { AgentEventInput } from "../events/types";
|
|
2
|
+
import { type AskUserRequest, type AskUserResponse } from "../tools/types";
|
|
3
|
+
import {
|
|
4
|
+
type AskUserResolution,
|
|
5
|
+
type AskUserSnapshot,
|
|
6
|
+
type BashGuardSnapshot,
|
|
7
|
+
type BashGuardSource,
|
|
8
|
+
type CreateRuntimeSessionInput,
|
|
9
|
+
} from "./runtime-session-contracts";
|
|
10
|
+
import { cancellationError } from "./turn-cancellation";
|
|
11
|
+
import type { ToolCallIdentity } from "./types";
|
|
12
|
+
|
|
13
|
+
export class RuntimeInteractions {
|
|
14
|
+
private bashGuardMode: "guard" | "yolo";
|
|
15
|
+
private bashGuardSource: BashGuardSource;
|
|
16
|
+
private bashGuardSnapshot: BashGuardSnapshot;
|
|
17
|
+
private readonly bashGuardListeners = new Set<() => void>();
|
|
18
|
+
private askUserSnapshot: AskUserSnapshot = Object.freeze({});
|
|
19
|
+
private readonly askUserListeners = new Set<() => void>();
|
|
20
|
+
private pendingAskUser?: {
|
|
21
|
+
readonly request: AskUserRequest;
|
|
22
|
+
readonly startedAt: number;
|
|
23
|
+
readonly call: ToolCallIdentity;
|
|
24
|
+
readonly resolve: (response: AskUserResponse) => void;
|
|
25
|
+
readonly reject: (error: unknown) => void;
|
|
26
|
+
readonly removeAbortListener: () => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
private pendingBashConfirmation?: {
|
|
30
|
+
readonly command: string;
|
|
31
|
+
readonly reason: string;
|
|
32
|
+
readonly startedAt: number;
|
|
33
|
+
readonly call: ToolCallIdentity;
|
|
34
|
+
readonly resolve: (decision: "allow" | "deny") => void;
|
|
35
|
+
readonly reject: (error: unknown) => void;
|
|
36
|
+
readonly removeAbortListener: () => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly bashGuardConfig: CreateRuntimeSessionInput["bashGuard"],
|
|
41
|
+
private readonly append: (event: AgentEventInput) => Promise<void>,
|
|
42
|
+
) {
|
|
43
|
+
this.bashGuardMode = bashGuardConfig?.mode ?? "guard";
|
|
44
|
+
this.bashGuardSource = bashGuardConfig?.source ?? "default";
|
|
45
|
+
this.bashGuardSnapshot = Object.freeze({
|
|
46
|
+
mode: this.bashGuardMode,
|
|
47
|
+
source: this.bashGuardSource,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bashGuard(): BashGuardSnapshot {
|
|
52
|
+
return this.bashGuardSnapshot;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private refreshBashGuardSnapshot(): void {
|
|
56
|
+
this.bashGuardSnapshot = Object.freeze({
|
|
57
|
+
mode: this.bashGuardMode,
|
|
58
|
+
source: this.bashGuardSource,
|
|
59
|
+
...(this.pendingBashConfirmation === undefined
|
|
60
|
+
? {}
|
|
61
|
+
: {
|
|
62
|
+
pending: Object.freeze({
|
|
63
|
+
command: this.pendingBashConfirmation.command,
|
|
64
|
+
reason: this.pendingBashConfirmation.reason,
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
subscribeBashGuard(listener: () => void): () => void {
|
|
71
|
+
this.bashGuardListeners.add(listener);
|
|
72
|
+
return () => this.bashGuardListeners.delete(listener);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setYoloMode(enabled: boolean): void {
|
|
76
|
+
this.bashGuardMode = enabled ? "yolo" : "guard";
|
|
77
|
+
this.bashGuardSource = "session";
|
|
78
|
+
this.refreshBashGuardSnapshot();
|
|
79
|
+
this.notifyBashGuardListeners();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async resolveBashConfirmation(decision: "allow" | "deny"): Promise<void> {
|
|
83
|
+
const pending = this.pendingBashConfirmation;
|
|
84
|
+
if (pending === undefined) {
|
|
85
|
+
throw new Error("No Bash confirmation is pending.");
|
|
86
|
+
}
|
|
87
|
+
this.pendingBashConfirmation = undefined;
|
|
88
|
+
this.refreshBashGuardSnapshot();
|
|
89
|
+
pending.removeAbortListener();
|
|
90
|
+
await this.append({
|
|
91
|
+
type: "tool.confirmation.resolved",
|
|
92
|
+
...pending.call,
|
|
93
|
+
data: {
|
|
94
|
+
command: pending.command,
|
|
95
|
+
reason: pending.reason,
|
|
96
|
+
decision,
|
|
97
|
+
durationMs: Date.now() - pending.startedAt,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
pending.resolve(decision);
|
|
101
|
+
this.notifyBashGuardListeners();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async confirmBashCommand(
|
|
105
|
+
call: ToolCallIdentity,
|
|
106
|
+
request: { command: string; reason: string },
|
|
107
|
+
signal: AbortSignal,
|
|
108
|
+
): Promise<"allow" | "deny"> {
|
|
109
|
+
const startedAt = Date.now();
|
|
110
|
+
await this.append({
|
|
111
|
+
type: "tool.confirmation.requested",
|
|
112
|
+
...call,
|
|
113
|
+
data: request,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const surface = this.bashGuardConfig?.surface ?? "one-shot";
|
|
117
|
+
if (this.bashGuardMode === "yolo" || surface === "one-shot") {
|
|
118
|
+
const decision = this.bashGuardMode === "yolo" ? "allow" : "deny";
|
|
119
|
+
await this.append({
|
|
120
|
+
type: "tool.confirmation.resolved",
|
|
121
|
+
...call,
|
|
122
|
+
data: {
|
|
123
|
+
...request,
|
|
124
|
+
decision,
|
|
125
|
+
durationMs: Date.now() - startedAt,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
return decision;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (this.pendingBashConfirmation !== undefined) {
|
|
132
|
+
throw new Error("Another Bash confirmation is already pending.");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return new Promise<"allow" | "deny">((resolve, reject) => {
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
const pending = this.pendingBashConfirmation;
|
|
138
|
+
if (pending?.call.toolCallId !== call.toolCallId) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.pendingBashConfirmation = undefined;
|
|
142
|
+
this.refreshBashGuardSnapshot();
|
|
143
|
+
void this.append({
|
|
144
|
+
type: "tool.confirmation.resolved",
|
|
145
|
+
...call,
|
|
146
|
+
data: {
|
|
147
|
+
...request,
|
|
148
|
+
decision: "cancelled",
|
|
149
|
+
durationMs: Date.now() - startedAt,
|
|
150
|
+
},
|
|
151
|
+
}).finally(() => {
|
|
152
|
+
reject(cancellationError(signal));
|
|
153
|
+
this.notifyBashGuardListeners();
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
157
|
+
this.pendingBashConfirmation = {
|
|
158
|
+
...request,
|
|
159
|
+
startedAt,
|
|
160
|
+
call,
|
|
161
|
+
resolve,
|
|
162
|
+
reject,
|
|
163
|
+
removeAbortListener: () => signal.removeEventListener("abort", onAbort),
|
|
164
|
+
};
|
|
165
|
+
this.refreshBashGuardSnapshot();
|
|
166
|
+
this.notifyBashGuardListeners();
|
|
167
|
+
if (signal.aborted) {
|
|
168
|
+
onAbort();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private notifyBashGuardListeners(): void {
|
|
174
|
+
for (const listener of this.bashGuardListeners) {
|
|
175
|
+
listener();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
askUser(): AskUserSnapshot {
|
|
180
|
+
return this.askUserSnapshot;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
subscribeAskUser(listener: () => void): () => void {
|
|
184
|
+
this.askUserListeners.add(listener);
|
|
185
|
+
return () => this.askUserListeners.delete(listener);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async resolveAskUser(response: AskUserResolution): Promise<void> {
|
|
189
|
+
const pending = this.pendingAskUser;
|
|
190
|
+
if (pending === undefined) {
|
|
191
|
+
throw new Error("No AskUser question is pending.");
|
|
192
|
+
}
|
|
193
|
+
let result: AskUserResponse;
|
|
194
|
+
if (response.outcome === "selected") {
|
|
195
|
+
if (!Number.isSafeInteger(response.selectedIndex)) {
|
|
196
|
+
throw new Error("AskUser selectedIndex must be an integer.");
|
|
197
|
+
}
|
|
198
|
+
const option = pending.request.options[response.selectedIndex];
|
|
199
|
+
if (option === undefined) {
|
|
200
|
+
throw new Error("AskUser selectedIndex is out of range.");
|
|
201
|
+
}
|
|
202
|
+
result = { outcome: "selected", answer: option.description };
|
|
203
|
+
} else {
|
|
204
|
+
result = { outcome: "dismissed" };
|
|
205
|
+
}
|
|
206
|
+
this.pendingAskUser = undefined;
|
|
207
|
+
this.askUserSnapshot = Object.freeze({});
|
|
208
|
+
pending.removeAbortListener();
|
|
209
|
+
await this.append({
|
|
210
|
+
type: "tool.user_question.resolved",
|
|
211
|
+
...pending.call,
|
|
212
|
+
data: {
|
|
213
|
+
...result,
|
|
214
|
+
durationMs: Date.now() - pending.startedAt,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
pending.resolve(result);
|
|
218
|
+
this.notifyAskUserListeners();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async requestUserAnswer(
|
|
222
|
+
call: ToolCallIdentity,
|
|
223
|
+
request: AskUserRequest,
|
|
224
|
+
signal: AbortSignal,
|
|
225
|
+
): Promise<AskUserResponse> {
|
|
226
|
+
if (this.pendingAskUser !== undefined) {
|
|
227
|
+
throw new Error("Another AskUser question is already pending.");
|
|
228
|
+
}
|
|
229
|
+
if (this.pendingBashConfirmation !== undefined) {
|
|
230
|
+
throw new Error("Cannot ask the user while a Bash confirmation is pending.");
|
|
231
|
+
}
|
|
232
|
+
if (signal.aborted) {
|
|
233
|
+
throw cancellationError(signal);
|
|
234
|
+
}
|
|
235
|
+
const startedAt = Date.now();
|
|
236
|
+
await this.append({
|
|
237
|
+
type: "tool.user_question.requested",
|
|
238
|
+
...call,
|
|
239
|
+
data: request,
|
|
240
|
+
});
|
|
241
|
+
return new Promise<AskUserResponse>((resolve, reject) => {
|
|
242
|
+
const onAbort = () => {
|
|
243
|
+
const pending = this.pendingAskUser;
|
|
244
|
+
if (pending?.call.toolCallId !== call.toolCallId) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.pendingAskUser = undefined;
|
|
248
|
+
this.askUserSnapshot = Object.freeze({});
|
|
249
|
+
void this.append({
|
|
250
|
+
type: "tool.user_question.resolved",
|
|
251
|
+
...call,
|
|
252
|
+
data: {
|
|
253
|
+
outcome: "cancelled",
|
|
254
|
+
durationMs: Date.now() - startedAt,
|
|
255
|
+
},
|
|
256
|
+
}).finally(() => {
|
|
257
|
+
reject(cancellationError(signal));
|
|
258
|
+
this.notifyAskUserListeners();
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
262
|
+
const immutableRequest = Object.freeze({
|
|
263
|
+
question: request.question,
|
|
264
|
+
options: Object.freeze(
|
|
265
|
+
request.options.map((option) =>
|
|
266
|
+
Object.freeze({ description: option.description }),
|
|
267
|
+
),
|
|
268
|
+
),
|
|
269
|
+
});
|
|
270
|
+
this.pendingAskUser = {
|
|
271
|
+
request: immutableRequest,
|
|
272
|
+
startedAt,
|
|
273
|
+
call,
|
|
274
|
+
resolve,
|
|
275
|
+
reject,
|
|
276
|
+
removeAbortListener: () => signal.removeEventListener("abort", onAbort),
|
|
277
|
+
};
|
|
278
|
+
this.askUserSnapshot = Object.freeze({ pending: immutableRequest });
|
|
279
|
+
this.notifyAskUserListeners();
|
|
280
|
+
if (signal.aborted) {
|
|
281
|
+
onAbort();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private notifyAskUserListeners(): void {
|
|
287
|
+
for (const listener of this.askUserListeners) {
|
|
288
|
+
listener();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { AgentEventInput } from "../events/types";
|
|
2
|
+
import { validateUserMessage, type UserMessage } from "../image/image-types";
|
|
3
|
+
import {
|
|
4
|
+
type AcceptedTurn,
|
|
5
|
+
type ExecuteTurnInput,
|
|
6
|
+
type PromptSchedulerSnapshot,
|
|
7
|
+
type QueueFollowUpResult,
|
|
8
|
+
type RuntimeSessionState,
|
|
9
|
+
} from "./runtime-session-contracts";
|
|
10
|
+
import { type AgentTurnLedger } from "./session-ledger";
|
|
11
|
+
import type { RunAgentResult, TurnIdentity } from "./types";
|
|
12
|
+
import { projectUserMessage } from "./user-prompt-projection";
|
|
13
|
+
|
|
14
|
+
type QueuedPrompt = {
|
|
15
|
+
readonly userMessage: UserMessage;
|
|
16
|
+
};
|
|
17
|
+
const MAX_QUEUED_PROMPTS = 8;
|
|
18
|
+
const MAX_QUEUED_PROMPT_TEXT_BYTES = 64 * 1024;
|
|
19
|
+
/** Owns queued prompts and their execution chain; admission stays in the runtime. */
|
|
20
|
+
export class RuntimePromptScheduler {
|
|
21
|
+
private executionChainRunning = false;
|
|
22
|
+
private readonly queuedPrompts: QueuedPrompt[] = [];
|
|
23
|
+
private promptSchedulerSnapshot: PromptSchedulerSnapshot = Object.freeze({
|
|
24
|
+
state: "idle",
|
|
25
|
+
pendingCount: 0,
|
|
26
|
+
});
|
|
27
|
+
private readonly promptSchedulerListeners = new Set<() => void>();
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly getState: () => RuntimeSessionState,
|
|
30
|
+
private readonly getActiveTurn: () => { turn: TurnIdentity } | undefined,
|
|
31
|
+
private readonly admitSingleTurn: (
|
|
32
|
+
input: ExecuteTurnInput,
|
|
33
|
+
) => Promise<AcceptedTurn>,
|
|
34
|
+
private readonly append: (event: AgentEventInput) => Promise<void>,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
get isRunning(): boolean {
|
|
38
|
+
return this.executionChainRunning;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get pendingCount(): number {
|
|
42
|
+
return this.queuedPrompts.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear(): void {
|
|
46
|
+
this.queuedPrompts.splice(0);
|
|
47
|
+
this.executionChainRunning = false;
|
|
48
|
+
this.notifyPromptScheduler();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
promptScheduler(): PromptSchedulerSnapshot {
|
|
52
|
+
return this.promptSchedulerSnapshot;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
subscribePromptScheduler(listener: () => void): () => void {
|
|
56
|
+
this.promptSchedulerListeners.add(listener);
|
|
57
|
+
return () => this.promptSchedulerListeners.delete(listener);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
queueFollowUp(userMessage: UserMessage): QueueFollowUpResult {
|
|
61
|
+
if (!this.executionChainRunning) {
|
|
62
|
+
throw new Error("Cannot queue a follow-up while no execution chain is running.");
|
|
63
|
+
}
|
|
64
|
+
validateUserMessage(userMessage);
|
|
65
|
+
if (userMessage.attachments !== undefined) {
|
|
66
|
+
throw new Error("Active-turn follow-ups do not support image attachments.");
|
|
67
|
+
}
|
|
68
|
+
if (this.queuedPrompts.length >= MAX_QUEUED_PROMPTS) {
|
|
69
|
+
throw new Error(`At most ${MAX_QUEUED_PROMPTS} follow-ups may be queued.`);
|
|
70
|
+
}
|
|
71
|
+
const queuedBytes = this.queuedPrompts.reduce(
|
|
72
|
+
(total, entry) => total + Buffer.byteLength(entry.userMessage.content, "utf8"),
|
|
73
|
+
0,
|
|
74
|
+
);
|
|
75
|
+
const nextBytes = Buffer.byteLength(userMessage.content, "utf8");
|
|
76
|
+
if (queuedBytes + nextBytes > MAX_QUEUED_PROMPT_TEXT_BYTES) {
|
|
77
|
+
throw new Error("Queued follow-ups exceed the 64 KiB text limit.");
|
|
78
|
+
}
|
|
79
|
+
this.queuedPrompts.push({
|
|
80
|
+
userMessage: Object.freeze({ ...userMessage }),
|
|
81
|
+
});
|
|
82
|
+
this.notifyPromptScheduler();
|
|
83
|
+
const activeTurn = this.getActiveTurn();
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
kind: "queued",
|
|
86
|
+
pendingCount: this.queuedPrompts.length,
|
|
87
|
+
...(activeTurn === undefined ? {} : { activeTurnId: activeTurn.turn.turnId }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
|
|
92
|
+
if (this.executionChainRunning) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Cannot execute a turn while RuntimeSession is ${this.getState()}; a prompt chain is already executing.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
this.executionChainRunning = true;
|
|
98
|
+
this.notifyPromptScheduler();
|
|
99
|
+
try {
|
|
100
|
+
const accepted = await this.admitSingleTurn(input);
|
|
101
|
+
const completion = this.continueExecutionChain(accepted.completion, input.signal);
|
|
102
|
+
return Object.freeze({ ...accepted, completion });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.executionChainRunning = false;
|
|
105
|
+
this.notifyPromptScheduler();
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async continueExecutionChain(
|
|
111
|
+
initialCompletion: Promise<RunAgentResult>,
|
|
112
|
+
signal: AbortSignal,
|
|
113
|
+
): Promise<RunAgentResult> {
|
|
114
|
+
let completion = initialCompletion;
|
|
115
|
+
let finalResult: RunAgentResult;
|
|
116
|
+
try {
|
|
117
|
+
for (;;) {
|
|
118
|
+
finalResult = await completion;
|
|
119
|
+
if (finalResult.status !== "completed" || this.queuedPrompts.length === 0) {
|
|
120
|
+
return finalResult;
|
|
121
|
+
}
|
|
122
|
+
const next = this.queuedPrompts[0];
|
|
123
|
+
if (next === undefined) {
|
|
124
|
+
return finalResult;
|
|
125
|
+
}
|
|
126
|
+
const accepted = await this.admitSingleTurn({
|
|
127
|
+
userMessage: next.userMessage,
|
|
128
|
+
signal,
|
|
129
|
+
});
|
|
130
|
+
this.queuedPrompts.shift();
|
|
131
|
+
this.notifyPromptScheduler();
|
|
132
|
+
completion = accepted.completion;
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
this.queuedPrompts.splice(0);
|
|
136
|
+
this.executionChainRunning = false;
|
|
137
|
+
this.notifyPromptScheduler();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
notifyPromptScheduler(): void {
|
|
142
|
+
const activeTurn = this.getActiveTurn();
|
|
143
|
+
this.promptSchedulerSnapshot = Object.freeze({
|
|
144
|
+
state: this.executionChainRunning ? "running" : "idle",
|
|
145
|
+
...(activeTurn === undefined ? {} : { activeTurnId: activeTurn.turn.turnId }),
|
|
146
|
+
pendingCount: this.queuedPrompts.length,
|
|
147
|
+
});
|
|
148
|
+
for (const listener of this.promptSchedulerListeners) listener();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async applyQueuedSteering(input: {
|
|
152
|
+
turn: TurnIdentity;
|
|
153
|
+
ledger: AgentTurnLedger;
|
|
154
|
+
}): Promise<number> {
|
|
155
|
+
const activeTurn = this.getActiveTurn();
|
|
156
|
+
if (activeTurn?.turn.turnId !== input.turn.turnId) {
|
|
157
|
+
throw new Error("Cannot apply steering outside the active turn.");
|
|
158
|
+
}
|
|
159
|
+
if (this.queuedPrompts.length === 0) return 0;
|
|
160
|
+
const drained = this.queuedPrompts.splice(0);
|
|
161
|
+
const records = input.ledger.appendSteeringUserMessages(
|
|
162
|
+
drained.map((entry) => entry.userMessage),
|
|
163
|
+
);
|
|
164
|
+
this.notifyPromptScheduler();
|
|
165
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
166
|
+
const record = records[index];
|
|
167
|
+
const queued = drained[index];
|
|
168
|
+
if (record === undefined || queued === undefined) {
|
|
169
|
+
throw new Error("Steering ledger result did not match the drained queue.");
|
|
170
|
+
}
|
|
171
|
+
await this.append({
|
|
172
|
+
type: "turn.steering.applied",
|
|
173
|
+
...input.turn,
|
|
174
|
+
data: {
|
|
175
|
+
userPrompt: projectUserMessage(queued.userMessage),
|
|
176
|
+
ordinal: record.ordinal,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return records.length;
|
|
181
|
+
}
|
|
182
|
+
}
|