@agentswf/engine 0.1.0-beta.4 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/agent-cli/cli.ts +57 -17
- package/src/attempt-ending.ts +2 -4
- package/src/attempt-output.ts +2 -1
- package/src/attempt-view.ts +1 -1
- package/src/control-plane.ts +37 -5
- package/src/coordinator-socket.ts +15 -6
- package/src/deadlines.ts +69 -2
- package/src/decisions/directory.ts +9 -11
- package/src/deprecations.ts +177 -0
- package/src/duration.ts +50 -20
- package/src/emit-command.ts +55 -41
- package/src/emit.ts +124 -15
- package/src/help/emit.txt +12 -4
- package/src/help/inbox.txt +2 -2
- package/src/help/logs.txt +5 -4
- package/src/help/reply.txt +5 -5
- package/src/help/run.txt +21 -7
- package/src/herdr-run-session.ts +2 -2
- package/src/holder-command.ts +36 -31
- package/src/inbox-command.ts +7 -10
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +2 -2
- package/src/keep-alive.ts +28 -13
- package/src/logs-command.ts +12 -12
- package/src/mailbox.ts +2 -20
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +26 -21
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +7 -5
- package/src/operator-cli.ts +23 -9
- package/src/operator-runtime.ts +5 -5
- package/src/pane-layout.ts +125 -32
- package/src/progress-view.ts +8 -8
- package/src/run-command.ts +25 -4
- package/src/run-commands.ts +1 -1
- package/src/run-prepare.ts +46 -8
- package/src/run-usage.ts +1 -1
- package/src/run-view.ts +9 -4
- package/src/runs-command.ts +12 -10
- package/src/runs.ts +44 -8
- package/src/sources.ts +42 -6
- package/src/stage-ledger.ts +42 -12
- package/src/stage-plan.ts +37 -1
- package/src/test-command.ts +7 -3
- package/src/testing.ts +7 -8
- package/src/tools.ts +46 -18
- package/src/workflow-loader.ts +5 -2
- package/src/workflow-runner.ts +553 -235
- package/src/workflow-testing/coordinator.typecheck.ts +7 -7
- package/src/workflow-testing/coordinators.ts +2 -2
- package/src/workflow-testing/host.ts +107 -38
- package/src/workflow-testing/index.ts +45 -836
- package/src/workflow-testing/run-test.ts +1213 -0
- package/src/workflow-testing/script.ts +102 -41
- package/src/workflow-testing/workflow-testing.typecheck.ts +11 -8
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Checked by `tsc --noEmit`, never run: story 032's workflow and its test, as an author writes them.
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import {
|
|
3
|
+
defineWorkflow,
|
|
4
|
+
type ParticipantRef,
|
|
5
|
+
type WorkflowContext,
|
|
6
6
|
} from "@agentswf/contract/workflow";
|
|
7
7
|
import { answer, type ParticipantScript, testWorkflow } from ".";
|
|
8
8
|
|
|
9
9
|
declare const runtime: string;
|
|
10
10
|
|
|
11
|
-
const investigate
|
|
11
|
+
const investigate = defineWorkflow<null, string>({
|
|
12
12
|
meta: { name: "investigate", description: "watches checkout-api" },
|
|
13
13
|
async run(wf: WorkflowContext) {
|
|
14
14
|
const investigator = await wf.agents.open({ key: "investigator", runtime });
|
|
@@ -27,10 +27,10 @@ const investigate: WorkflowDefinition<null, string> = {
|
|
|
27
27
|
const left = wf.deadline ? wf.deadline.unixMilliseconds - Date.now() : undefined;
|
|
28
28
|
return outcome.answered ? `${outcome.value} (${left ?? "no"} ms left)` : outcome.kind;
|
|
29
29
|
},
|
|
30
|
-
};
|
|
30
|
+
});
|
|
31
31
|
|
|
32
32
|
export async function story(): Promise<string> {
|
|
33
|
-
const run = await testWorkflow(investigate,
|
|
33
|
+
const run = await testWorkflow(investigate, {
|
|
34
34
|
agents: { investigator: [answer("…")] },
|
|
35
35
|
participants: {
|
|
36
36
|
coordinator: (event) =>
|
|
@@ -100,7 +100,7 @@ export class ParticipantScripts implements CoordinatorPort {
|
|
|
100
100
|
const script = this.#scripts[coordinator];
|
|
101
101
|
if (!script) {
|
|
102
102
|
this.#events.onScriptError(
|
|
103
|
-
`${event.kind === "route" ? "a route" : "a message"} reached
|
|
103
|
+
`${event.kind === "route" ? "a route" : "a message"} reached participant "${coordinator}", which has no script under participants`,
|
|
104
104
|
);
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
@@ -111,7 +111,7 @@ export class ParticipantScripts implements CoordinatorPort {
|
|
|
111
111
|
.then(() => script(event))
|
|
112
112
|
.catch((error: unknown) =>
|
|
113
113
|
this.#events.onScriptError(
|
|
114
|
-
`
|
|
114
|
+
`participant "${coordinator}"'s script threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
115
115
|
),
|
|
116
116
|
)
|
|
117
117
|
.finally(() => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SandboxRecord } from "@agentswf/contract/records";
|
|
1
|
+
import type { SandboxRecord, TurnRecord } from "@agentswf/contract/records";
|
|
2
2
|
import {
|
|
3
3
|
type MessageResponse,
|
|
4
4
|
REPLY_CODES,
|
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
type JsonObject,
|
|
18
18
|
type KeepPane,
|
|
19
19
|
type PaneLayout,
|
|
20
|
+
type PaneSpec,
|
|
20
21
|
placementOf,
|
|
22
|
+
positiveDurationMilliseconds,
|
|
21
23
|
} from "@agentswf/contract/workflow";
|
|
22
24
|
import {
|
|
23
25
|
createPlacementHostFactory,
|
|
@@ -38,27 +40,30 @@ import {
|
|
|
38
40
|
type FakeSet,
|
|
39
41
|
} from "@agentswf/harness/testing";
|
|
40
42
|
import { submitControl, submitResult, submitWaiting } from "../agent-cli/client";
|
|
43
|
+
import type { Deprecated } from "../deprecations";
|
|
41
44
|
import {
|
|
45
|
+
deprecatedAs,
|
|
42
46
|
type Message,
|
|
43
47
|
type MessageOutcome,
|
|
44
48
|
type MessageScripts,
|
|
49
|
+
noAnswer,
|
|
50
|
+
outcomeOf,
|
|
45
51
|
type ReceivedMessage,
|
|
46
|
-
reply,
|
|
47
52
|
type Scripts,
|
|
48
53
|
type SendOptions,
|
|
49
54
|
type Step,
|
|
55
|
+
type TestTurnOutcome,
|
|
50
56
|
type ToolOutcome,
|
|
51
57
|
type Turn,
|
|
52
|
-
type TurnOutcome,
|
|
53
58
|
} from "./script";
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* A turn as the workflow wrote it, and how it ended. `messages` are those it was shown, absent when
|
|
57
62
|
* none were.
|
|
58
63
|
*/
|
|
59
|
-
export type
|
|
64
|
+
export type TestTurn = Omit<Turn, "signal" | "messages" | "send" | "reply" | "tool"> & {
|
|
60
65
|
messages?: readonly Message[];
|
|
61
|
-
outcome:
|
|
66
|
+
outcome: TestTurnOutcome;
|
|
62
67
|
};
|
|
63
68
|
|
|
64
69
|
/** A compaction as the workflow asked for it, and how it ended. */
|
|
@@ -69,7 +74,7 @@ export type CompactionRecord = {
|
|
|
69
74
|
id: string;
|
|
70
75
|
/** The spec's prompt: what the harness is to keep and drop. */
|
|
71
76
|
focus: string;
|
|
72
|
-
outcome:
|
|
77
|
+
outcome: TestTurnOutcome;
|
|
73
78
|
};
|
|
74
79
|
|
|
75
80
|
/** A switch of an agent's model or effort, as the host was asked for it. */
|
|
@@ -98,9 +103,11 @@ export type OpenedAgent = {
|
|
|
98
103
|
*/
|
|
99
104
|
forkedFrom?: { key: string; turns: number } | { caller: true; turns: number };
|
|
100
105
|
/**
|
|
101
|
-
* Its pane
|
|
102
|
-
* pane landed or whether a `beside` fell back.
|
|
106
|
+
* Its `pane` as the host was asked to place it: its `tab`, or `beside` another, and `front`. This
|
|
107
|
+
* host has no screen, so nothing says where a pane landed or whether a `beside` fell back.
|
|
103
108
|
*/
|
|
109
|
+
pane?: PaneSpec;
|
|
110
|
+
/** What the host was asked, a deprecated `layout`'s session and workspace included. */
|
|
104
111
|
layout?: PaneLayout;
|
|
105
112
|
/** Why the engine already knew `layout` could not be used: a `beside` an agent with no pane. */
|
|
106
113
|
layoutFallback?: string;
|
|
@@ -114,12 +121,17 @@ export type AgentSandbox = Pick<SandboxRecord, "key" | "provider" | "spec" | "do
|
|
|
114
121
|
|
|
115
122
|
export type ScriptedHost = {
|
|
116
123
|
factory: AgentRunHostFactory;
|
|
117
|
-
turns:
|
|
124
|
+
turns: TestTurn[];
|
|
118
125
|
compactions: CompactionRecord[];
|
|
119
126
|
sets: SetRecord[];
|
|
120
127
|
agents: OpenedAgent[];
|
|
121
128
|
/** Turns started and not yet ended, for a stalled run's message. */
|
|
122
|
-
inFlight():
|
|
129
|
+
inFlight(): TestTurn[];
|
|
130
|
+
/**
|
|
131
|
+
* Once the run has ended, gives each operation's last turn, and each compaction, the outcome the
|
|
132
|
+
* engine recorded for it in `turns`: a turn before that, a nudge's or a wait's, keeps its own.
|
|
133
|
+
*/
|
|
134
|
+
settle(recorded: readonly TurnRecord[]): void;
|
|
123
135
|
};
|
|
124
136
|
|
|
125
137
|
/**
|
|
@@ -137,8 +149,10 @@ export function createScriptedHost(
|
|
|
137
149
|
onActivity(): void;
|
|
138
150
|
/** The script could not meet a turn: the test fails. */
|
|
139
151
|
onScriptError(message: string): void;
|
|
152
|
+
/** A script ended a turn through a deprecated name, for the run to log. */
|
|
153
|
+
onDeprecated(name: Deprecated, subject?: string): void;
|
|
140
154
|
/** A turn began, before its script answers: what a test sends during it goes now. */
|
|
141
|
-
onTurnStarted?(turn:
|
|
155
|
+
onTurnStarted?(turn: TestTurn): Promise<void>;
|
|
142
156
|
},
|
|
143
157
|
/**
|
|
144
158
|
* The session the run is started from, as `awf run --here` finds one, and the model its files
|
|
@@ -146,12 +160,14 @@ export function createScriptedHost(
|
|
|
146
160
|
*/
|
|
147
161
|
caller?: { harness: string; cwd: string; model?: string; here?: boolean },
|
|
148
162
|
): ScriptedHost {
|
|
149
|
-
const turns:
|
|
163
|
+
const turns: TestTurn[] = [];
|
|
150
164
|
const compactions: CompactionRecord[] = [];
|
|
151
165
|
const compactionCounts = new Map<string, number>();
|
|
152
166
|
const sets: SetRecord[] = [];
|
|
153
167
|
const agents: OpenedAgent[] = [];
|
|
154
|
-
const open = new Set<
|
|
168
|
+
const open = new Set<TestTurn>();
|
|
169
|
+
/** Each turn's operation, which the engine's record of it names. */
|
|
170
|
+
const operationOf = new Map<TestTurn, string>();
|
|
155
171
|
const counts = new Map<string, number>();
|
|
156
172
|
/** Each fork's session, by the agent it copied and that agent's turns so far. */
|
|
157
173
|
const forks = new Map<string, NonNullable<OpenedAgent["forkedFrom"]>>();
|
|
@@ -172,7 +188,7 @@ export function createScriptedHost(
|
|
|
172
188
|
turns: callerKey === undefined ? 0 : (counts.get(callerKey) ?? 0),
|
|
173
189
|
}),
|
|
174
190
|
}));
|
|
175
|
-
const end = (record:
|
|
191
|
+
const end = (record: TestTurn, outcome: TestTurnOutcome) => {
|
|
176
192
|
record.outcome = outcome;
|
|
177
193
|
open.delete(record);
|
|
178
194
|
events.onActivity();
|
|
@@ -206,13 +222,20 @@ export function createScriptedHost(
|
|
|
206
222
|
failTest(why);
|
|
207
223
|
throw new Error(why);
|
|
208
224
|
}
|
|
225
|
+
if (options.timeout != null && options.timeoutMs != null)
|
|
226
|
+
throw new Error(`a ${command}'s timeout is given alone: timeoutMs is its deprecated form`);
|
|
227
|
+
const timeoutMs =
|
|
228
|
+
options.timeout == null
|
|
229
|
+
? (options.timeoutMs ?? undefined)
|
|
230
|
+
: positiveDurationMilliseconds(options.timeout, `a ${command}'s timeout`);
|
|
231
|
+
if (options.timeoutMs != null) events.onDeprecated("timeout-ms", `on a script's ${command}`);
|
|
209
232
|
const call = submitControl(operation.binding.endpoint, {
|
|
210
233
|
version: WIRE_VERSION,
|
|
211
234
|
command,
|
|
212
235
|
peer,
|
|
213
236
|
text,
|
|
214
237
|
...(options.awaitReply ? { awaitReply: true as const } : {}),
|
|
215
|
-
...(
|
|
238
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
216
239
|
});
|
|
217
240
|
invocation.calls.add(call);
|
|
218
241
|
let response: MessageResponse;
|
|
@@ -304,7 +327,7 @@ export function createScriptedHost(
|
|
|
304
327
|
const invoke = (
|
|
305
328
|
context: FakeAdapterTurnContext,
|
|
306
329
|
operation: Operation,
|
|
307
|
-
record:
|
|
330
|
+
record: TestTurn,
|
|
308
331
|
shown: readonly Message[],
|
|
309
332
|
): Invocation => {
|
|
310
333
|
const { activation, authored } = context;
|
|
@@ -338,8 +361,8 @@ export function createScriptedHost(
|
|
|
338
361
|
reply: (text, options) => turn.reply(woke!.from, text, options),
|
|
339
362
|
send: turn.send,
|
|
340
363
|
tool: turn.tool,
|
|
341
|
-
silent:
|
|
342
|
-
fail: (
|
|
364
|
+
silent: deprecatedAs("message-silent", noAnswer.silent),
|
|
365
|
+
fail: deprecatedAs("message-fail", noAnswer.failed),
|
|
343
366
|
} satisfies ReceivedMessage,
|
|
344
367
|
operation.message!,
|
|
345
368
|
);
|
|
@@ -353,7 +376,9 @@ export function createScriptedHost(
|
|
|
353
376
|
if (was === "running") invocation.stopped.resolve({ step: taken });
|
|
354
377
|
else if (was === "suspended") {
|
|
355
378
|
failTest(
|
|
356
|
-
|
|
379
|
+
taken.kind === "script-error"
|
|
380
|
+
? taken.message
|
|
381
|
+
: `agent "${turn.agent}" turn ${turn.n}: its script went on after its turn ended awaiting a reply; await only the host's calls`,
|
|
357
382
|
);
|
|
358
383
|
}
|
|
359
384
|
});
|
|
@@ -364,7 +389,7 @@ export function createScriptedHost(
|
|
|
364
389
|
const until = async (
|
|
365
390
|
invocation: Invocation,
|
|
366
391
|
context: FakeAdapterTurnContext,
|
|
367
|
-
record:
|
|
392
|
+
record: TestTurn,
|
|
368
393
|
): Promise<FakeAdapterTurn> => {
|
|
369
394
|
const { operation } = invocation;
|
|
370
395
|
const stopped = await Promise.race([invocation.stopped.promise, whenAborted(context.signal)]);
|
|
@@ -400,7 +425,7 @@ export function createScriptedHost(
|
|
|
400
425
|
const perform = (
|
|
401
426
|
step: Step,
|
|
402
427
|
context: FakeAdapterTurnContext,
|
|
403
|
-
record:
|
|
428
|
+
record: TestTurn,
|
|
404
429
|
invocation?: Invocation,
|
|
405
430
|
): FakeAdapterTurn => {
|
|
406
431
|
if (step.kind === "script-error") {
|
|
@@ -411,7 +436,7 @@ export function createScriptedHost(
|
|
|
411
436
|
return {
|
|
412
437
|
act: async () => {
|
|
413
438
|
// Submitted is answered, even though the engine then releases the turn.
|
|
414
|
-
let outcome:
|
|
439
|
+
let outcome: TestTurnOutcome = "cancelled";
|
|
415
440
|
try {
|
|
416
441
|
// As an agent's `wfa send` returns before it can run `wfa result`.
|
|
417
442
|
if (invocation) await Promise.allSettled([...invocation.calls]);
|
|
@@ -443,6 +468,7 @@ export function createScriptedHost(
|
|
|
443
468
|
};
|
|
444
469
|
}
|
|
445
470
|
const { ending } = step;
|
|
471
|
+
if (step.deprecated) events.onDeprecated(step.deprecated);
|
|
446
472
|
if (ending.kind === "waiting") {
|
|
447
473
|
return {
|
|
448
474
|
act: async () => {
|
|
@@ -465,7 +491,7 @@ export function createScriptedHost(
|
|
|
465
491
|
return {
|
|
466
492
|
act: async (turn) => {
|
|
467
493
|
await whenAborted(turn.signal);
|
|
468
|
-
end(record, "
|
|
494
|
+
end(record, "cancelled");
|
|
469
495
|
},
|
|
470
496
|
};
|
|
471
497
|
}
|
|
@@ -476,10 +502,10 @@ export function createScriptedHost(
|
|
|
476
502
|
`agent "${record.agent}" turn ${record.n}: only the calling session can be interrupted by the operator`,
|
|
477
503
|
);
|
|
478
504
|
}
|
|
479
|
-
end(record, "
|
|
505
|
+
end(record, "cancelled");
|
|
480
506
|
return { state: "cancelled", detail: "interrupted by the operator" };
|
|
481
507
|
}
|
|
482
|
-
end(record, ending.kind);
|
|
508
|
+
end(record, outcomeOf(ending.kind));
|
|
483
509
|
return ended(ending, context.activation.execution.harness);
|
|
484
510
|
};
|
|
485
511
|
|
|
@@ -492,10 +518,11 @@ export function createScriptedHost(
|
|
|
492
518
|
n,
|
|
493
519
|
id: context.id,
|
|
494
520
|
focus: context.prompt,
|
|
495
|
-
|
|
521
|
+
// Until it ends: one the run leaves running is cancelled with it.
|
|
522
|
+
outcome: "cancelled",
|
|
496
523
|
};
|
|
497
524
|
compactions.push(record);
|
|
498
|
-
const done = (outcome:
|
|
525
|
+
const done = (outcome: TestTurnOutcome) => {
|
|
499
526
|
record.outcome = outcome;
|
|
500
527
|
events.onActivity();
|
|
501
528
|
};
|
|
@@ -541,12 +568,13 @@ export function createScriptedHost(
|
|
|
541
568
|
return { summary: step.value };
|
|
542
569
|
}
|
|
543
570
|
const { ending } = step;
|
|
571
|
+
if (step.deprecated) events.onDeprecated(step.deprecated);
|
|
544
572
|
if (ending.kind === "waiting") return failTest("compaction cannot report waiting");
|
|
545
573
|
if (ending.kind === "hang") {
|
|
546
574
|
return {
|
|
547
575
|
act: async (turn) => {
|
|
548
576
|
await whenAborted(turn.signal);
|
|
549
|
-
done("
|
|
577
|
+
done("cancelled");
|
|
550
578
|
},
|
|
551
579
|
};
|
|
552
580
|
}
|
|
@@ -555,7 +583,7 @@ export function createScriptedHost(
|
|
|
555
583
|
`agent "${activation.key}" compaction ${n}: a compaction is not interrupted by the operator`,
|
|
556
584
|
);
|
|
557
585
|
}
|
|
558
|
-
done(ending.kind);
|
|
586
|
+
done(outcomeOf(ending.kind));
|
|
559
587
|
return ended(ending, activation.execution.harness);
|
|
560
588
|
};
|
|
561
589
|
|
|
@@ -591,7 +619,7 @@ export function createScriptedHost(
|
|
|
591
619
|
]
|
|
592
620
|
: [],
|
|
593
621
|
);
|
|
594
|
-
const record:
|
|
622
|
+
const record: TestTurn = {
|
|
595
623
|
agent: activation.key,
|
|
596
624
|
n,
|
|
597
625
|
// A continuation that delivers nothing is a reminder of a reply owed: a nudge.
|
|
@@ -605,9 +633,11 @@ export function createScriptedHost(
|
|
|
605
633
|
...(context.settings.effort === undefined ? {} : { effort: context.settings.effort }),
|
|
606
634
|
...(authored.cause === undefined ? {} : { cause: authored.cause }),
|
|
607
635
|
...(shown.length === 0 ? {} : { messages: shown }),
|
|
608
|
-
|
|
636
|
+
// Until it ends: one the run leaves running is cancelled with it.
|
|
637
|
+
outcome: "cancelled",
|
|
609
638
|
};
|
|
610
639
|
turns.push(record);
|
|
640
|
+
operationOf.set(record, binding.operationId);
|
|
611
641
|
open.add(record);
|
|
612
642
|
if (!record.nudge && context.kind === "turn") await events.onTurnStarted?.(record);
|
|
613
643
|
|
|
@@ -657,7 +687,7 @@ export function createScriptedHost(
|
|
|
657
687
|
createFakeAdapter({
|
|
658
688
|
harnesses: PLACEMENT_HARNESSES[placement],
|
|
659
689
|
supportsWaiting: (activation) =>
|
|
660
|
-
placement === "
|
|
690
|
+
placement === "tui" &&
|
|
661
691
|
activation.execution.harness === "claude" &&
|
|
662
692
|
!activation.execution.caller,
|
|
663
693
|
placement,
|
|
@@ -690,7 +720,7 @@ export function createScriptedHost(
|
|
|
690
720
|
}),
|
|
691
721
|
);
|
|
692
722
|
const placed = createPlacementHostFactory({
|
|
693
|
-
|
|
723
|
+
tui: side("tui"),
|
|
694
724
|
headless: side("headless"),
|
|
695
725
|
...(caller && caller.here !== false
|
|
696
726
|
? {
|
|
@@ -701,7 +731,7 @@ export function createScriptedHost(
|
|
|
701
731
|
createSingleSessionHostFactory(
|
|
702
732
|
createFakeAdapter({
|
|
703
733
|
harnesses: [caller.harness],
|
|
704
|
-
placement: "
|
|
734
|
+
placement: "tui",
|
|
705
735
|
script: (context) => {
|
|
706
736
|
callerKey = context.activation.key;
|
|
707
737
|
return script(context);
|
|
@@ -736,6 +766,7 @@ export function createScriptedHost(
|
|
|
736
766
|
...(request.continues && forks.has(request.continues.sessionRef)
|
|
737
767
|
? { forkedFrom: forks.get(request.continues.sessionRef)! }
|
|
738
768
|
: {}),
|
|
769
|
+
...paneOf(request),
|
|
739
770
|
...(request.layout ? { layout: request.layout } : {}),
|
|
740
771
|
...(request.layoutFallback ? { layoutFallback: request.layoutFallback } : {}),
|
|
741
772
|
...(request.keepPane ? { keepPane: request.keepPane } : {}),
|
|
@@ -758,6 +789,26 @@ export function createScriptedHost(
|
|
|
758
789
|
sets,
|
|
759
790
|
agents,
|
|
760
791
|
inFlight: () => [...open],
|
|
792
|
+
settle(recorded) {
|
|
793
|
+
const engine = new Map(
|
|
794
|
+
recorded.flatMap((line) =>
|
|
795
|
+
line.kind === "turn" ? [[line.operationId, line.outcome]] : [],
|
|
796
|
+
),
|
|
797
|
+
);
|
|
798
|
+
const last = new Map<string, TestTurn>();
|
|
799
|
+
for (const turn of turns) last.set(operationOf.get(turn)!, turn);
|
|
800
|
+
for (const [operation, turn] of last) turn.outcome = engine.get(operation) ?? turn.outcome;
|
|
801
|
+
// Matched by place: the harness's compact() carries no operation id, and an agent's compactions run in order,
|
|
802
|
+
// one `compact` line per compaction the host saw.
|
|
803
|
+
const compacted = recorded.filter((line) => line.kind === "compact");
|
|
804
|
+
const seen = new Map<string, number>();
|
|
805
|
+
for (const compaction of compactions) {
|
|
806
|
+
const n = seen.get(compaction.agent) ?? 0;
|
|
807
|
+
seen.set(compaction.agent, n + 1);
|
|
808
|
+
const line = compacted.filter((each) => each.agent === compaction.agent)[n];
|
|
809
|
+
if (line) compaction.outcome = line.outcome;
|
|
810
|
+
}
|
|
811
|
+
},
|
|
761
812
|
};
|
|
762
813
|
}
|
|
763
814
|
|
|
@@ -816,12 +867,30 @@ async function noTools(): Promise<ToolOutcome> {
|
|
|
816
867
|
throw new Error("a compaction calls no tools");
|
|
817
868
|
}
|
|
818
869
|
|
|
870
|
+
/** The workflow's part of what the host was asked for a pane, as `pane` writes it. */
|
|
871
|
+
function paneOf({ layout, front }: HarnessActivation): { pane?: PaneSpec } {
|
|
872
|
+
if (layout?.beside !== undefined) {
|
|
873
|
+
const { beside, side, share } = layout;
|
|
874
|
+
return {
|
|
875
|
+
pane: {
|
|
876
|
+
beside,
|
|
877
|
+
side,
|
|
878
|
+
...(share === undefined ? {} : { share }),
|
|
879
|
+
...(front ? { front } : {}),
|
|
880
|
+
},
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const tab = layout?.tab;
|
|
884
|
+
if (tab === undefined && !front) return {};
|
|
885
|
+
return { pane: { ...(tab === undefined ? {} : { tab }), ...(front ? { front } : {}) } };
|
|
886
|
+
}
|
|
887
|
+
|
|
819
888
|
/** What the pane host refuses before it opens a pane. */
|
|
820
889
|
function paneRefusal(activation: HarnessActivation): string | undefined {
|
|
821
890
|
const { harness } = activation.execution;
|
|
822
891
|
const spec = findHarness(harness);
|
|
823
892
|
if (activation.continues && spec && !spec.interactiveResume) {
|
|
824
|
-
return `${harness}
|
|
893
|
+
return `${harness} tui agents cannot continue a forked session: ${spec.absent.interactiveResume}`;
|
|
825
894
|
}
|
|
826
895
|
return undefined;
|
|
827
896
|
}
|
|
@@ -839,7 +908,7 @@ function refusedCompaction(activation: HarnessActivation, hasRun: boolean): stri
|
|
|
839
908
|
}
|
|
840
909
|
const spec = findHarness(activation.execution.harness);
|
|
841
910
|
const capability =
|
|
842
|
-
placementOf(activation.execution) === "
|
|
911
|
+
placementOf(activation.execution) === "tui" ? "compactPane" : "compactHeadless";
|
|
843
912
|
if (spec && !spec[capability]) {
|
|
844
913
|
return `${activation.execution.harness} has no compaction of its own: ${spec.absent[capability]}`;
|
|
845
914
|
}
|
|
@@ -848,7 +917,7 @@ function refusedCompaction(activation: HarnessActivation, hasRun: boolean): stri
|
|
|
848
917
|
}
|
|
849
918
|
|
|
850
919
|
type Ended = Exclude<
|
|
851
|
-
Extract<Step, { kind: "
|
|
920
|
+
Extract<Step, { kind: "no-answer" }>["ending"],
|
|
852
921
|
{ kind: "waiting" | "hang" | "interrupted" }
|
|
853
922
|
>;
|
|
854
923
|
|
|
@@ -861,7 +930,7 @@ function ended(ending: Ended, harness: string): FakeAdapterTurn {
|
|
|
861
930
|
return check
|
|
862
931
|
? {
|
|
863
932
|
state: "failed",
|
|
864
|
-
...loginFailure(harness, check, { ...provider, said: "
|
|
933
|
+
...loginFailure(harness, check, { ...provider, said: "noAnswer.needsLogin()" }),
|
|
865
934
|
}
|
|
866
935
|
: {
|
|
867
936
|
state: "failed",
|