@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.
Files changed (56) hide show
  1. package/package.json +4 -4
  2. package/src/agent-cli/cli.ts +57 -17
  3. package/src/attempt-ending.ts +2 -4
  4. package/src/attempt-output.ts +2 -1
  5. package/src/attempt-view.ts +1 -1
  6. package/src/control-plane.ts +37 -5
  7. package/src/coordinator-socket.ts +15 -6
  8. package/src/deadlines.ts +69 -2
  9. package/src/decisions/directory.ts +9 -11
  10. package/src/deprecations.ts +177 -0
  11. package/src/duration.ts +50 -20
  12. package/src/emit-command.ts +55 -41
  13. package/src/emit.ts +124 -15
  14. package/src/help/emit.txt +12 -4
  15. package/src/help/inbox.txt +2 -2
  16. package/src/help/logs.txt +5 -4
  17. package/src/help/reply.txt +5 -5
  18. package/src/help/run.txt +21 -7
  19. package/src/herdr-run-session.ts +2 -2
  20. package/src/holder-command.ts +36 -31
  21. package/src/inbox-command.ts +7 -10
  22. package/src/init-template/hello/workflow.test.ts.template +3 -3
  23. package/src/init-template/hello/workflow.ts.template +2 -2
  24. package/src/keep-alive.ts +28 -13
  25. package/src/logs-command.ts +12 -12
  26. package/src/mailbox.ts +2 -20
  27. package/src/messaging/limits.ts +4 -4
  28. package/src/messaging/messaging.ts +26 -21
  29. package/src/messaging/routes.ts +1 -1
  30. package/src/messaging/text.ts +7 -5
  31. package/src/operator-cli.ts +23 -9
  32. package/src/operator-runtime.ts +5 -5
  33. package/src/pane-layout.ts +125 -32
  34. package/src/progress-view.ts +8 -8
  35. package/src/run-command.ts +25 -4
  36. package/src/run-commands.ts +1 -1
  37. package/src/run-prepare.ts +46 -8
  38. package/src/run-usage.ts +1 -1
  39. package/src/run-view.ts +9 -4
  40. package/src/runs-command.ts +12 -10
  41. package/src/runs.ts +44 -8
  42. package/src/sources.ts +42 -6
  43. package/src/stage-ledger.ts +42 -12
  44. package/src/stage-plan.ts +37 -1
  45. package/src/test-command.ts +7 -3
  46. package/src/testing.ts +7 -8
  47. package/src/tools.ts +46 -18
  48. package/src/workflow-loader.ts +5 -2
  49. package/src/workflow-runner.ts +553 -235
  50. package/src/workflow-testing/coordinator.typecheck.ts +7 -7
  51. package/src/workflow-testing/coordinators.ts +2 -2
  52. package/src/workflow-testing/host.ts +107 -38
  53. package/src/workflow-testing/index.ts +45 -836
  54. package/src/workflow-testing/run-test.ts +1213 -0
  55. package/src/workflow-testing/script.ts +102 -41
  56. 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 type {
3
- ParticipantRef,
4
- WorkflowContext,
5
- WorkflowDefinition,
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: WorkflowDefinition<null, string> = {
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, null, {
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 coordinator "${coordinator}", which has no script under coordinators`,
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
- `coordinator "${coordinator}"'s script threw: ${error instanceof Error ? error.message : String(error)}`,
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 TurnRecord = Omit<Turn, "signal" | "messages" | "send" | "reply" | "tool"> & {
64
+ export type TestTurn = Omit<Turn, "signal" | "messages" | "send" | "reply" | "tool"> & {
60
65
  messages?: readonly Message[];
61
- outcome: TurnOutcome;
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: TurnOutcome;
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'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.
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: TurnRecord[];
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(): TurnRecord[];
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: TurnRecord): Promise<void>;
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: TurnRecord[] = [];
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<TurnRecord>();
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: TurnRecord, outcome: TurnOutcome) => {
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
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
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: TurnRecord,
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: reply.silent,
342
- fail: (reason) => reply.failed(reason),
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
- `agent "${turn.agent}" turn ${turn.n}: its script went on after its turn ended awaiting a reply; await only the host's calls`,
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: TurnRecord,
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: TurnRecord,
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: TurnOutcome = "cancelled";
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, "hang");
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, "interrupted");
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
- outcome: "hang",
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: TurnOutcome) => {
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("hang");
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: TurnRecord = {
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
- outcome: "hang",
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 === "pane" &&
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
- pane: side("pane"),
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: "pane",
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} panes cannot continue a forked session: ${spec.absent.interactiveResume}`;
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) === "pane" ? "compactPane" : "compactHeadless";
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: "reply" }>["ending"],
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: "reply.needsLogin()" }),
933
+ ...loginFailure(harness, check, { ...provider, said: "noAnswer.needsLogin()" }),
865
934
  }
866
935
  : {
867
936
  state: "failed",