@agentswf/engine 0.1.0-beta.4 → 0.1.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/package.json +13 -9
  4. package/src/accounting/format.ts +4 -3
  5. package/src/accounting/spent.ts +128 -0
  6. package/src/accounting/summary.ts +40 -32
  7. package/src/agent-cli/cli.ts +35 -22
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +5 -7
  11. package/src/attempt-output.ts +15 -32
  12. package/src/attempt-view.ts +34 -33
  13. package/src/author-typebox.ts +13 -0
  14. package/src/client/checks.ts +94 -0
  15. package/src/client/exported.ts +17 -0
  16. package/src/client/index.ts +122 -0
  17. package/src/client/locate.ts +34 -0
  18. package/src/client/published.ts +3 -0
  19. package/src/client/runs.ts +337 -0
  20. package/src/command-line.ts +64 -0
  21. package/src/commands.ts +7 -1
  22. package/src/control-plane.ts +41 -11
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +41 -33
  25. package/src/deadlines.ts +40 -7
  26. package/src/decisions/directory.ts +51 -40
  27. package/src/decisions/fake.ts +1 -1
  28. package/src/decisions/openrouter.ts +4 -2
  29. package/src/decisions/seam.ts +4 -4
  30. package/src/display-path.ts +3 -3
  31. package/src/duration.ts +43 -27
  32. package/src/emit-command.ts +88 -58
  33. package/src/emit.ts +254 -56
  34. package/src/event-ids.ts +31 -0
  35. package/src/files.ts +44 -4
  36. package/src/hand-over.ts +150 -0
  37. package/src/help/allowance.txt +4 -3
  38. package/src/help/emit.txt +18 -6
  39. package/src/help/inbox.txt +6 -4
  40. package/src/help/logs.txt +9 -6
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +30 -11
  44. package/src/help/runs.txt +4 -2
  45. package/src/help/tick.txt +9 -4
  46. package/src/herdr-placement.ts +3 -33
  47. package/src/herdr-run-session.ts +7 -15
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +56 -133
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +40 -57
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.test.ts.template +3 -3
  55. package/src/init-template/hello/workflow.ts.template +5 -4
  56. package/src/jsonl.ts +10 -6
  57. package/src/keep-alive.ts +15 -12
  58. package/src/logs-command.ts +128 -47
  59. package/src/message-operation.ts +6 -3
  60. package/src/messaging/limits.ts +4 -4
  61. package/src/messaging/messaging.ts +31 -28
  62. package/src/messaging/routes.ts +1 -1
  63. package/src/messaging/text.ts +9 -7
  64. package/src/open-command.ts +770 -0
  65. package/src/operation-events.ts +4 -6
  66. package/src/operation-liveness.ts +19 -4
  67. package/src/operator-cli.ts +224 -94
  68. package/src/operator-runtime.ts +29 -7
  69. package/src/ops.ts +270 -0
  70. package/src/pane-layout.ts +105 -59
  71. package/src/progress-view.ts +10 -10
  72. package/src/project-engine.ts +79 -0
  73. package/src/projects.ts +13 -2
  74. package/src/refusals.ts +119 -0
  75. package/src/result-slots.ts +11 -31
  76. package/src/result-validation.ts +2 -2
  77. package/src/run-command.ts +45 -28
  78. package/src/run-commands.ts +43 -115
  79. package/src/run-event-log.ts +1 -1
  80. package/src/run-lines.ts +159 -0
  81. package/src/run-lock.ts +57 -1
  82. package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
  83. package/src/run-policy/stages.ts +113 -0
  84. package/src/run-policy/waits.ts +452 -0
  85. package/src/run-prepare.ts +205 -43
  86. package/src/run-read.ts +109 -41
  87. package/src/run-state.ts +170 -50
  88. package/src/run-text.ts +54 -0
  89. package/src/run-unreachable.ts +18 -0
  90. package/src/run-usage.ts +18 -12
  91. package/src/run-view.ts +81 -17
  92. package/src/runs-command.ts +83 -63
  93. package/src/runs.ts +310 -212
  94. package/src/sandbox-homes.ts +65 -13
  95. package/src/sandboxes.ts +182 -28
  96. package/src/session-lines.ts +93 -0
  97. package/src/skills/run-skills.ts +8 -1
  98. package/src/sources.ts +28 -29
  99. package/src/spend.ts +333 -0
  100. package/src/stage-ledger.ts +119 -131
  101. package/src/stop-crashed.ts +6 -5
  102. package/src/stopped.ts +4 -4
  103. package/src/test-command.ts +36 -14
  104. package/src/testing/operation-events.ts +3 -23
  105. package/src/testing.ts +73 -26
  106. package/src/tick-command.ts +145 -15
  107. package/src/tools.ts +25 -16
  108. package/src/turn-ends.ts +37 -0
  109. package/src/waits.ts +651 -0
  110. package/src/wake.ts +84 -54
  111. package/src/workflow-loader.ts +54 -4
  112. package/src/workflow-runner.ts +805 -580
  113. package/src/workflow-test-preload.ts +4 -1
  114. package/src/workflow-testing/coordinators.ts +3 -3
  115. package/src/workflow-testing/fake-client.ts +387 -0
  116. package/src/workflow-testing/host.ts +121 -53
  117. package/src/workflow-testing/index.ts +33 -842
  118. package/src/workflow-testing/run-test.ts +1128 -0
  119. package/src/workflow-testing/script.ts +74 -45
  120. package/src/hold-run-lock.fixture.ts +0 -8
  121. package/src/mailbox.ts +0 -990
  122. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  123. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
@@ -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,
@@ -16,8 +16,10 @@ import {
16
16
  type Effort,
17
17
  type JsonObject,
18
18
  type KeepPane,
19
- type PaneLayout,
19
+ type PaneSpec,
20
+ type PaneWorkspace,
20
21
  placementOf,
22
+ positiveDurationMilliseconds,
21
23
  } from "@agentswf/contract/workflow";
22
24
  import {
23
25
  createPlacementHostFactory,
@@ -37,28 +39,28 @@ import {
37
39
  type FakeFork,
38
40
  type FakeSet,
39
41
  } from "@agentswf/harness/testing";
40
- import { submitControl, submitResult, submitWaiting } from "../agent-cli/client";
42
+ import { submitControl } from "../agent-cli/client";
41
43
  import {
42
44
  type Message,
43
45
  type MessageOutcome,
44
46
  type MessageScripts,
47
+ outcomeOf,
45
48
  type ReceivedMessage,
46
- reply,
47
49
  type Scripts,
48
50
  type SendOptions,
49
51
  type Step,
52
+ type TestTurnOutcome,
50
53
  type ToolOutcome,
51
54
  type Turn,
52
- type TurnOutcome,
53
55
  } from "./script";
54
56
 
55
57
  /**
56
58
  * A turn as the workflow wrote it, and how it ended. `messages` are those it was shown, absent when
57
59
  * none were.
58
60
  */
59
- export type TurnRecord = Omit<Turn, "signal" | "messages" | "send" | "reply" | "tool"> & {
61
+ export type TestTurn = Omit<Turn, "signal" | "messages" | "send" | "reply" | "tool"> & {
60
62
  messages?: readonly Message[];
61
- outcome: TurnOutcome;
63
+ outcome: TestTurnOutcome;
62
64
  };
63
65
 
64
66
  /** A compaction as the workflow asked for it, and how it ended. */
@@ -69,7 +71,7 @@ export type CompactionRecord = {
69
71
  id: string;
70
72
  /** The spec's prompt: what the harness is to keep and drop. */
71
73
  focus: string;
72
- outcome: TurnOutcome;
74
+ outcome: TestTurnOutcome;
73
75
  };
74
76
 
75
77
  /** A switch of an agent's model or effort, as the host was asked for it. */
@@ -87,7 +89,6 @@ export type OpenedAgent = {
87
89
  key: string;
88
90
  execution: AgentExecution;
89
91
  instructions?: string;
90
- labels?: JsonObject;
91
92
  /** The skills it was given by name; absent when the workflow named none. */
92
93
  skills?: readonly string[];
93
94
  /** The sandbox it ran in; absent on the host. */
@@ -98,28 +99,37 @@ export type OpenedAgent = {
98
99
  */
99
100
  forkedFrom?: { key: string; turns: number } | { caller: true; turns: number };
100
101
  /**
101
- * Its pane's layout as the workflow wrote it; this host has no screen, so nothing says where a
102
- * pane landed or whether a `beside` fell back.
102
+ * Its `pane` as the host was asked to place it: its `tab`, or `beside` another, and `front`. This
103
+ * host has no screen, so nothing says where a pane landed or whether a `beside` fell back.
103
104
  */
104
- layout?: PaneLayout;
105
- /** Why the engine already knew `layout` could not be used: a `beside` an agent with no pane. */
106
- layoutFallback?: string;
107
- keepPane?: KeepPane;
108
- /** Its pane was kept when it was done: `keepPane` asked, and how its last operation ended let it. */
105
+ pane?: PaneSpec;
106
+ /**
107
+ * The run's `panes` as the host was asked to apply them to this pane: the `workspace` its tab
108
+ * opens in, which a `beside` takes from the pane it splits, and whether it is `keep`.
109
+ */
110
+ panes?: { workspace?: PaneWorkspace; keep?: KeepPane };
111
+ /** Why the engine already knew `pane` could not be used: a `beside` an agent with no pane. */
112
+ paneFallback?: string;
113
+ /** Its pane was kept when it was done: `panes.keep` asked, and its last operation's end let it. */
109
114
  keptPane?: true;
110
115
  };
111
116
 
112
117
  /** The sandbox an agent ran in, as the run's record keeps it. */
113
118
  export type AgentSandbox = Pick<SandboxRecord, "key" | "provider" | "spec" | "domains">;
114
119
 
115
- export type ScriptedHost = {
120
+ type ScriptedHost = {
116
121
  factory: AgentRunHostFactory;
117
- turns: TurnRecord[];
122
+ turns: TestTurn[];
118
123
  compactions: CompactionRecord[];
119
124
  sets: SetRecord[];
120
125
  agents: OpenedAgent[];
121
126
  /** Turns started and not yet ended, for a stalled run's message. */
122
- inFlight(): TurnRecord[];
127
+ inFlight(): TestTurn[];
128
+ /**
129
+ * Once the run has ended, gives each operation's last turn, and each compaction, the outcome the
130
+ * engine recorded for it in `turns`: a turn before that, a nudge's or a wait's, keeps its own.
131
+ */
132
+ settle(recorded: readonly TurnRecord[]): void;
123
133
  };
124
134
 
125
135
  /**
@@ -138,7 +148,7 @@ export function createScriptedHost(
138
148
  /** The script could not meet a turn: the test fails. */
139
149
  onScriptError(message: string): void;
140
150
  /** A turn began, before its script answers: what a test sends during it goes now. */
141
- onTurnStarted?(turn: TurnRecord): Promise<void>;
151
+ onTurnStarted?(turn: TestTurn): Promise<void>;
142
152
  },
143
153
  /**
144
154
  * The session the run is started from, as `awf run --here` finds one, and the model its files
@@ -146,12 +156,14 @@ export function createScriptedHost(
146
156
  */
147
157
  caller?: { harness: string; cwd: string; model?: string; here?: boolean },
148
158
  ): ScriptedHost {
149
- const turns: TurnRecord[] = [];
159
+ const turns: TestTurn[] = [];
150
160
  const compactions: CompactionRecord[] = [];
151
161
  const compactionCounts = new Map<string, number>();
152
162
  const sets: SetRecord[] = [];
153
163
  const agents: OpenedAgent[] = [];
154
- const open = new Set<TurnRecord>();
164
+ const open = new Set<TestTurn>();
165
+ /** Each turn's operation, which the engine's record of it names. */
166
+ const operationOf = new Map<TestTurn, string>();
155
167
  const counts = new Map<string, number>();
156
168
  /** Each fork's session, by the agent it copied and that agent's turns so far. */
157
169
  const forks = new Map<string, NonNullable<OpenedAgent["forkedFrom"]>>();
@@ -172,7 +184,7 @@ export function createScriptedHost(
172
184
  turns: callerKey === undefined ? 0 : (counts.get(callerKey) ?? 0),
173
185
  }),
174
186
  }));
175
- const end = (record: TurnRecord, outcome: TurnOutcome) => {
187
+ const end = (record: TestTurn, outcome: TestTurnOutcome) => {
176
188
  record.outcome = outcome;
177
189
  open.delete(record);
178
190
  events.onActivity();
@@ -206,13 +218,17 @@ export function createScriptedHost(
206
218
  failTest(why);
207
219
  throw new Error(why);
208
220
  }
221
+ const timeoutMs =
222
+ options.timeout == null
223
+ ? undefined
224
+ : positiveDurationMilliseconds(options.timeout, `a ${command}'s timeout`);
209
225
  const call = submitControl(operation.binding.endpoint, {
210
226
  version: WIRE_VERSION,
211
227
  command,
212
228
  peer,
213
229
  text,
214
230
  ...(options.awaitReply ? { awaitReply: true as const } : {}),
215
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
231
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
216
232
  });
217
233
  invocation.calls.add(call);
218
234
  let response: MessageResponse;
@@ -304,7 +320,7 @@ export function createScriptedHost(
304
320
  const invoke = (
305
321
  context: FakeAdapterTurnContext,
306
322
  operation: Operation,
307
- record: TurnRecord,
323
+ record: TestTurn,
308
324
  shown: readonly Message[],
309
325
  ): Invocation => {
310
326
  const { activation, authored } = context;
@@ -338,8 +354,6 @@ export function createScriptedHost(
338
354
  reply: (text, options) => turn.reply(woke!.from, text, options),
339
355
  send: turn.send,
340
356
  tool: turn.tool,
341
- silent: reply.silent,
342
- fail: (reason) => reply.failed(reason),
343
357
  } satisfies ReceivedMessage,
344
358
  operation.message!,
345
359
  );
@@ -353,7 +367,9 @@ export function createScriptedHost(
353
367
  if (was === "running") invocation.stopped.resolve({ step: taken });
354
368
  else if (was === "suspended") {
355
369
  failTest(
356
- `agent "${turn.agent}" turn ${turn.n}: its script went on after its turn ended awaiting a reply; await only the host's calls`,
370
+ taken.kind === "script-error"
371
+ ? taken.message
372
+ : `agent "${turn.agent}" turn ${turn.n}: its script went on after its turn ended awaiting a reply; await only the host's calls`,
357
373
  );
358
374
  }
359
375
  });
@@ -364,7 +380,7 @@ export function createScriptedHost(
364
380
  const until = async (
365
381
  invocation: Invocation,
366
382
  context: FakeAdapterTurnContext,
367
- record: TurnRecord,
383
+ record: TestTurn,
368
384
  ): Promise<FakeAdapterTurn> => {
369
385
  const { operation } = invocation;
370
386
  const stopped = await Promise.race([invocation.stopped.promise, whenAborted(context.signal)]);
@@ -400,7 +416,7 @@ export function createScriptedHost(
400
416
  const perform = (
401
417
  step: Step,
402
418
  context: FakeAdapterTurnContext,
403
- record: TurnRecord,
419
+ record: TestTurn,
404
420
  invocation?: Invocation,
405
421
  ): FakeAdapterTurn => {
406
422
  if (step.kind === "script-error") {
@@ -411,12 +427,12 @@ export function createScriptedHost(
411
427
  return {
412
428
  act: async () => {
413
429
  // Submitted is answered, even though the engine then releases the turn.
414
- let outcome: TurnOutcome = "cancelled";
430
+ let outcome: TestTurnOutcome = "cancelled";
415
431
  try {
416
432
  // As an agent's `wfa send` returns before it can run `wfa result`.
417
433
  if (invocation) await Promise.allSettled([...invocation.calls]);
418
434
  const { endpoint, operationId } = context.binding!;
419
- const response = await submitResult(endpoint, {
435
+ const response = await submitControl(endpoint, {
420
436
  version: WIRE_VERSION,
421
437
  command: "result",
422
438
  operationId,
@@ -447,7 +463,7 @@ export function createScriptedHost(
447
463
  return {
448
464
  act: async () => {
449
465
  const { endpoint, operationId } = context.binding!;
450
- const response = await submitWaiting(endpoint, {
466
+ const response = await submitControl(endpoint, {
451
467
  version: WIRE_VERSION,
452
468
  command: "waiting",
453
469
  operationId,
@@ -465,7 +481,7 @@ export function createScriptedHost(
465
481
  return {
466
482
  act: async (turn) => {
467
483
  await whenAborted(turn.signal);
468
- end(record, "hang");
484
+ end(record, "cancelled");
469
485
  },
470
486
  };
471
487
  }
@@ -476,10 +492,10 @@ export function createScriptedHost(
476
492
  `agent "${record.agent}" turn ${record.n}: only the calling session can be interrupted by the operator`,
477
493
  );
478
494
  }
479
- end(record, "interrupted");
495
+ end(record, "cancelled");
480
496
  return { state: "cancelled", detail: "interrupted by the operator" };
481
497
  }
482
- end(record, ending.kind);
498
+ end(record, outcomeOf(ending.kind));
483
499
  return ended(ending, context.activation.execution.harness);
484
500
  };
485
501
 
@@ -492,10 +508,11 @@ export function createScriptedHost(
492
508
  n,
493
509
  id: context.id,
494
510
  focus: context.prompt,
495
- outcome: "hang",
511
+ // Until it ends: one the run leaves running is cancelled with it.
512
+ outcome: "cancelled",
496
513
  };
497
514
  compactions.push(record);
498
- const done = (outcome: TurnOutcome) => {
515
+ const done = (outcome: TestTurnOutcome) => {
499
516
  record.outcome = outcome;
500
517
  events.onActivity();
501
518
  };
@@ -546,7 +563,7 @@ export function createScriptedHost(
546
563
  return {
547
564
  act: async (turn) => {
548
565
  await whenAborted(turn.signal);
549
- done("hang");
566
+ done("cancelled");
550
567
  },
551
568
  };
552
569
  }
@@ -555,7 +572,7 @@ export function createScriptedHost(
555
572
  `agent "${activation.key}" compaction ${n}: a compaction is not interrupted by the operator`,
556
573
  );
557
574
  }
558
- done(ending.kind);
575
+ done(outcomeOf(ending.kind));
559
576
  return ended(ending, activation.execution.harness);
560
577
  };
561
578
 
@@ -591,7 +608,7 @@ export function createScriptedHost(
591
608
  ]
592
609
  : [],
593
610
  );
594
- const record: TurnRecord = {
611
+ const record: TestTurn = {
595
612
  agent: activation.key,
596
613
  n,
597
614
  // A continuation that delivers nothing is a reminder of a reply owed: a nudge.
@@ -605,9 +622,11 @@ export function createScriptedHost(
605
622
  ...(context.settings.effort === undefined ? {} : { effort: context.settings.effort }),
606
623
  ...(authored.cause === undefined ? {} : { cause: authored.cause }),
607
624
  ...(shown.length === 0 ? {} : { messages: shown }),
608
- outcome: "hang",
625
+ // Until it ends: one the run leaves running is cancelled with it.
626
+ outcome: "cancelled",
609
627
  };
610
628
  turns.push(record);
629
+ operationOf.set(record, binding.operationId);
611
630
  open.add(record);
612
631
  if (!record.nudge && context.kind === "turn") await events.onTurnStarted?.(record);
613
632
 
@@ -657,7 +676,7 @@ export function createScriptedHost(
657
676
  createFakeAdapter({
658
677
  harnesses: PLACEMENT_HARNESSES[placement],
659
678
  supportsWaiting: (activation) =>
660
- placement === "pane" &&
679
+ placement === "tui" &&
661
680
  activation.execution.harness === "claude" &&
662
681
  !activation.execution.caller,
663
682
  placement,
@@ -690,7 +709,7 @@ export function createScriptedHost(
690
709
  }),
691
710
  );
692
711
  const placed = createPlacementHostFactory({
693
- pane: side("pane"),
712
+ tui: side("tui"),
694
713
  headless: side("headless"),
695
714
  ...(caller && caller.here !== false
696
715
  ? {
@@ -701,7 +720,7 @@ export function createScriptedHost(
701
720
  createSingleSessionHostFactory(
702
721
  createFakeAdapter({
703
722
  harnesses: [caller.harness],
704
- placement: "pane",
723
+ placement: "tui",
705
724
  script: (context) => {
706
725
  callerKey = context.activation.key;
707
726
  return script(context);
@@ -731,14 +750,13 @@ export function createScriptedHost(
731
750
  key: request.key,
732
751
  execution: request.execution,
733
752
  ...(request.instructions ? { instructions: request.instructions } : {}),
734
- ...(request.labels ? { labels: request.labels } : {}),
735
753
  ...(request.skills ? { skills: request.skills.names } : {}),
736
754
  ...(request.continues && forks.has(request.continues.sessionRef)
737
755
  ? { forkedFrom: forks.get(request.continues.sessionRef)! }
738
756
  : {}),
739
- ...(request.layout ? { layout: request.layout } : {}),
740
- ...(request.layoutFallback ? { layoutFallback: request.layoutFallback } : {}),
741
- ...(request.keepPane ? { keepPane: request.keepPane } : {}),
757
+ ...paneOf(request),
758
+ ...panesOf(request),
759
+ ...(request.layoutFallback ? { paneFallback: request.layoutFallback } : {}),
742
760
  };
743
761
  agents.push(opened);
744
762
  return {
@@ -758,6 +776,26 @@ export function createScriptedHost(
758
776
  sets,
759
777
  agents,
760
778
  inFlight: () => [...open],
779
+ settle(recorded) {
780
+ const engine = new Map(
781
+ recorded.flatMap((line) =>
782
+ line.kind === "turn" ? [[line.operationId, line.outcome]] : [],
783
+ ),
784
+ );
785
+ const last = new Map<string, TestTurn>();
786
+ for (const turn of turns) last.set(operationOf.get(turn)!, turn);
787
+ for (const [operation, turn] of last) turn.outcome = engine.get(operation) ?? turn.outcome;
788
+ // Matched by place: the harness's compact() carries no operation id, and an agent's compactions run in order,
789
+ // one `compact` line per compaction the host saw.
790
+ const compacted = recorded.filter((line) => line.kind === "compact");
791
+ const seen = new Map<string, number>();
792
+ for (const compaction of compactions) {
793
+ const n = seen.get(compaction.agent) ?? 0;
794
+ seen.set(compaction.agent, n + 1);
795
+ const line = compacted.filter((each) => each.agent === compaction.agent)[n];
796
+ if (line) compaction.outcome = line.outcome;
797
+ }
798
+ },
761
799
  };
762
800
  }
763
801
 
@@ -816,12 +854,42 @@ async function noTools(): Promise<ToolOutcome> {
816
854
  throw new Error("a compaction calls no tools");
817
855
  }
818
856
 
857
+ /** The workflow's part of what the host was asked for a pane, as `pane` writes it. */
858
+ function paneOf({ layout, front }: HarnessActivation): { pane?: PaneSpec } {
859
+ if (layout?.beside !== undefined) {
860
+ const { beside, side, share } = layout;
861
+ return {
862
+ pane: {
863
+ beside,
864
+ side,
865
+ ...(share === undefined ? {} : { share }),
866
+ ...(front ? { front } : {}),
867
+ },
868
+ };
869
+ }
870
+ const tab = layout?.tab;
871
+ if (tab === undefined && !front) return {};
872
+ return { pane: { ...(tab === undefined ? {} : { tab }), ...(front ? { front } : {}) } };
873
+ }
874
+
875
+ /** The run's `panes` as the host was asked to apply them to this pane. */
876
+ function panesOf({ layout, keepPane }: HarnessActivation): Pick<OpenedAgent, "panes"> {
877
+ const workspace = layout?.beside === undefined ? layout?.workspace : undefined;
878
+ if (workspace === undefined && keepPane === undefined) return {};
879
+ return {
880
+ panes: {
881
+ ...(workspace === undefined ? {} : { workspace }),
882
+ ...(keepPane === undefined ? {} : { keep: keepPane }),
883
+ },
884
+ };
885
+ }
886
+
819
887
  /** What the pane host refuses before it opens a pane. */
820
888
  function paneRefusal(activation: HarnessActivation): string | undefined {
821
889
  const { harness } = activation.execution;
822
890
  const spec = findHarness(harness);
823
891
  if (activation.continues && spec && !spec.interactiveResume) {
824
- return `${harness} panes cannot continue a forked session: ${spec.absent.interactiveResume}`;
892
+ return `${harness} tui agents cannot continue a forked session: ${spec.absent.interactiveResume}`;
825
893
  }
826
894
  return undefined;
827
895
  }
@@ -839,7 +907,7 @@ function refusedCompaction(activation: HarnessActivation, hasRun: boolean): stri
839
907
  }
840
908
  const spec = findHarness(activation.execution.harness);
841
909
  const capability =
842
- placementOf(activation.execution) === "pane" ? "compactPane" : "compactHeadless";
910
+ placementOf(activation.execution) === "tui" ? "compactPane" : "compactHeadless";
843
911
  if (spec && !spec[capability]) {
844
912
  return `${activation.execution.harness} has no compaction of its own: ${spec.absent[capability]}`;
845
913
  }
@@ -848,7 +916,7 @@ function refusedCompaction(activation: HarnessActivation, hasRun: boolean): stri
848
916
  }
849
917
 
850
918
  type Ended = Exclude<
851
- Extract<Step, { kind: "reply" }>["ending"],
919
+ Extract<Step, { kind: "no-answer" }>["ending"],
852
920
  { kind: "waiting" | "hang" | "interrupted" }
853
921
  >;
854
922
 
@@ -861,7 +929,7 @@ function ended(ending: Ended, harness: string): FakeAdapterTurn {
861
929
  return check
862
930
  ? {
863
931
  state: "failed",
864
- ...loginFailure(harness, check, { ...provider, said: "reply.needsLogin()" }),
932
+ ...loginFailure(harness, check, { ...provider, said: "noAnswer.needsLogin()" }),
865
933
  }
866
934
  : {
867
935
  state: "failed",