@data-fair/lib-agents-sim 0.5.0 → 0.7.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/chat-driver.d.ts CHANGED
@@ -24,11 +24,31 @@ export declare function chatDriverStrings(locale: ChatDriverLocale): {
24
24
  };
25
25
  export declare const TURN_TIMEOUT_MS: number;
26
26
  export declare const SEND_TIMEOUT_MS = 15000;
27
+ /**
28
+ * How a turn ended. `waiting` means the assistant declared
29
+ * `wait_for_user_action` and is holding the turn open for the person — the
30
+ * caller's cue to let them act, then wait again for the turn it resumes.
31
+ */
32
+ export type TurnOutcome = 'ended' | 'waiting';
33
+ /**
34
+ * Matched on the activity's kind, not its label: the label is the model's own
35
+ * words interpolated into a translated string, so any text match would be both
36
+ * locale-dependent and at the mercy of what the assistant wrote.
37
+ */
38
+ /**
39
+ * How long an armed wait must persist before the driver calls it the person's
40
+ * move. Long enough for a resolving wait's indicator to clear, short enough to be
41
+ * nothing against a wait a person is actually thinking through.
42
+ */
43
+ export declare const WAIT_SETTLE_MS = 500;
44
+ export declare const WAITING_SELECTOR = "[data-testid=\"chat-activity\"][data-activity=\"waiting\"]";
27
45
  export declare function createChatDriver(root: ChatRoot, opts?: {
28
46
  locale?: ChatDriverLocale;
29
47
  }): {
30
- sendMessage(text: string): Promise<void>;
31
- waitForTurn(timeoutMs?: number): Promise<void>;
48
+ sendMessage(text: string, opts?: {
49
+ readyTimeoutMs?: number;
50
+ }): Promise<void>;
51
+ waitForTurn(timeoutMs?: number): Promise<TurnOutcome>;
32
52
  readConversation(): Promise<{
33
53
  role: "user" | "assistant";
34
54
  text: string;
package/chat-driver.js CHANGED
@@ -46,13 +46,34 @@ export const TURN_TIMEOUT_MS = 10 * 60 * 1000;
46
46
  // later with no diagnosis. Unrelated to TURN_TIMEOUT_MS, which bounds a
47
47
  // legitimately long model turn once the message has actually been sent.
48
48
  export const SEND_TIMEOUT_MS = 15000;
49
+ /**
50
+ * Matched on the activity's kind, not its label: the label is the model's own
51
+ * words interpolated into a translated string, so any text match would be both
52
+ * locale-dependent and at the mercy of what the assistant wrote.
53
+ */
54
+ /**
55
+ * How long an armed wait must persist before the driver calls it the person's
56
+ * move. Long enough for a resolving wait's indicator to clear, short enough to be
57
+ * nothing against a wait a person is actually thinking through.
58
+ */
59
+ export const WAIT_SETTLE_MS = 500;
60
+ export const WAITING_SELECTOR = '[data-testid="chat-activity"][data-activity="waiting"]';
49
61
  export function createChatDriver(root, opts = {}) {
50
62
  const strings = chatDriverStrings(opts.locale ?? 'en');
51
63
  return {
52
- async sendMessage(text) {
64
+ async sendMessage(text, opts = {}) {
53
65
  const fillAndSend = async () => {
54
66
  await root.getByPlaceholder(strings.input).fill(text, { timeout: SEND_TIMEOUT_MS });
55
- await root.getByRole('button', { name: strings.send }).click({ timeout: SEND_TIMEOUT_MS });
67
+ // Wait for the composer to be able to take it. While the assistant is
68
+ // genuinely working the send control IS the Stop button, so there is no
69
+ // Send to click — and a caller that tried anyway spent SEND_TIMEOUT_MS
70
+ // failing, pressed Escape, failed again, and left the text sitting in the
71
+ // box. A judged run lost six of its nine turns exactly so, and read as an
72
+ // assistant that had gone silent. Waiting for the turn is not a wedged
73
+ // page; it is the normal case, so it gets the caller's own ceiling.
74
+ const send = root.getByRole('button', { name: strings.send });
75
+ await send.waitFor({ state: 'visible', timeout: opts.readyTimeoutMs ?? SEND_TIMEOUT_MS });
76
+ await send.click({ timeout: SEND_TIMEOUT_MS });
56
77
  };
57
78
  try {
58
79
  await fillAndSend();
@@ -77,10 +98,36 @@ export function createChatDriver(root, opts = {}) {
77
98
  },
78
99
  async waitForTurn(timeoutMs = TURN_TIMEOUT_MS) {
79
100
  const stop = root.getByRole('button', { name: strings.stop });
101
+ const waiting = root.locator(WAITING_SELECTOR);
80
102
  // The turn may already be finished by the time we look, so a missing Stop
81
103
  // button is not an error — only one that never goes away is.
82
104
  await stop.waitFor({ state: 'visible', timeout: 15000 }).catch(() => { });
83
- await expect(stop).toHaveCount(0, { timeout: timeoutMs });
105
+ // A turn can finish two ways, and only one of them is the assistant being
106
+ // done. `wait_for_user_action` holds the turn open on purpose, having handed
107
+ // control back to the person — and a simulated person only acts between
108
+ // turns, so a harness that waited for the Stop button alone could never let
109
+ // them act on it. Every declared wait then ran its whole window and was
110
+ // recorded as a wedged turn; at a wait window as long as the harness's own
111
+ // ceiling, that is every run.
112
+ const ended = expect(stop).toHaveCount(0, { timeout: timeoutMs }).then(() => 'ended');
113
+ const armed = expect(waiting).toHaveCount(1, { timeout: timeoutMs }).then(
114
+ // Still armed a moment later, not merely armed at the instant we looked.
115
+ // A wait that the person has just resolved keeps its indicator for as long
116
+ // as the click takes to round-trip, and reporting THAT as "control is
117
+ // yours" hands the caller a turn that is already resuming underneath: the
118
+ // simulation loop then sends into a working turn, where the message used
119
+ // to be dropped in silence. Settling costs half a second on a real wait,
120
+ // which is a pause measured in minutes.
121
+ async () => {
122
+ await waiting.page().waitForTimeout(WAIT_SETTLE_MS);
123
+ if (await waiting.count() === 0)
124
+ return await new Promise(() => { });
125
+ return 'waiting';
126
+ },
127
+ // Never rejects: a wait that is simply not what this turn did must not be
128
+ // the error a caller sees. The Stop arm owns the timeout message.
129
+ () => new Promise(() => { }));
130
+ return await Promise.race([ended, armed]);
84
131
  },
85
132
  async readConversation() {
86
133
  // evaluateAll, not page.evaluate: FrameLocator has no evaluate, and this
package/index.d.ts CHANGED
@@ -6,6 +6,6 @@ export { writeEvidence, evidenceDir } from './transcript.ts';
6
6
  export { computeMetrics, type RunMetrics } from './metrics.ts';
7
7
  export { selectCases } from './cases.ts';
8
8
  export { reportCases } from './report.ts';
9
- export { createChatDriver, chatDriverStrings, type ChatRoot, type ChatDriverLocale, TURN_TIMEOUT_MS, SEND_TIMEOUT_MS } from './chat-driver.ts';
9
+ export { createChatDriver, chatDriverStrings, type ChatRoot, type ChatDriverLocale, type TurnOutcome, TURN_TIMEOUT_MS, SEND_TIMEOUT_MS, WAITING_SELECTOR } from './chat-driver.ts';
10
10
  export { createPagePerception, truncate, SNAPSHOT_CAP, ACTION_TIMEOUT_MS, MCP_SERVER_NAME as PAGE_MCP_SERVER_NAME } from './page-perception.ts';
11
11
  export type { PerceptionRoot, Observation, PagePerception } from './page-perception.ts';
package/index.js CHANGED
@@ -5,5 +5,5 @@ export { writeEvidence, evidenceDir } from "./transcript.js";
5
5
  export { computeMetrics } from "./metrics.js";
6
6
  export { selectCases } from "./cases.js";
7
7
  export { reportCases } from "./report.js";
8
- export { createChatDriver, chatDriverStrings, TURN_TIMEOUT_MS, SEND_TIMEOUT_MS } from "./chat-driver.js";
8
+ export { createChatDriver, chatDriverStrings, TURN_TIMEOUT_MS, SEND_TIMEOUT_MS, WAITING_SELECTOR } from "./chat-driver.js";
9
9
  export { createPagePerception, truncate, SNAPSHOT_CAP, ACTION_TIMEOUT_MS, MCP_SERVER_NAME as PAGE_MCP_SERVER_NAME } from "./page-perception.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-fair/lib-agents-sim",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Primitives for judged browser simulations of the data-fair agents chat, plus a Claude Code bridge exposing the Agent SDK as an OpenAI-compatible provider.",
5
5
  "main": "index.js",
6
6
  "type": "module",