negotium 0.1.37 → 0.1.39
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/README.md +19 -0
- package/dist/agent-helpers.js +15637 -5674
- package/dist/agent-helpers.js.map +85 -37
- package/dist/browser-runtime.js +1600 -0
- package/dist/browser-runtime.js.map +28 -0
- package/dist/hosted-agent.js +8 -8
- package/dist/hosted-agent.js.map +6 -5
- package/dist/main.js +3790 -3113
- package/dist/main.js.map +33 -29
- package/dist/mcp-factories.js +15141 -301
- package/dist/mcp-factories.js.map +100 -7
- package/dist/prompts.js +117 -49
- package/dist/prompts.js.map +3 -3
- package/dist/query-runtime.js +310 -1
- package/dist/query-runtime.js.map +10 -5
- package/dist/runtime/src/agents/archiver.ts +407 -296
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +440 -274
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +139 -65
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +1037 -816
- package/dist/runtime/src/agents/public-helpers.ts +82 -0
- package/dist/runtime/src/agents/self-config-core.ts +574 -278
- package/dist/runtime/src/agents/topic-cleanup.ts +167 -129
- package/dist/runtime/src/index.ts +4 -1
- package/dist/runtime/src/mcp/factories/index.ts +9 -0
- package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +132 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +23 -57
- package/dist/runtime/src/platform/mcp-config.ts +2 -4
- package/dist/runtime/src/platform/playwright/browser-processes.ts +3 -2
- package/dist/runtime/src/platform/playwright/capability.ts +5 -0
- package/dist/runtime/src/platform/playwright/manager.ts +380 -66
- package/dist/runtime/src/platform/playwright/public-runtime.ts +45 -0
- package/dist/runtime/src/platform/playwright/transport-probe.ts +89 -0
- package/dist/runtime/src/prompts/builders.ts +192 -51
- package/dist/runtime/src/query/active-rooms.ts +2 -0
- package/dist/runtime/src/query/public-runtime.ts +8 -0
- package/dist/runtime/src/query/state.ts +69 -33
- package/dist/runtime/src/query/types.ts +1 -0
- package/dist/runtime/src/runtime/playwright-turn-abort.ts +59 -0
- package/dist/runtime/src/runtime/turn-event-stream.ts +23 -6
- package/dist/runtime/src/runtime/turn-runner.ts +6 -1
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +38 -0
- package/dist/types/packages/core/src/agents/archiver.d.ts +52 -16
- package/dist/types/packages/core/src/agents/mcp-tools/ask-user.d.ts +75 -4
- package/dist/types/packages/core/src/agents/mcp-tools/self-config.d.ts +17 -0
- package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +112 -25
- package/dist/types/packages/core/src/agents/public-helpers.d.ts +7 -0
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +185 -0
- package/dist/types/packages/core/src/agents/topic-cleanup.d.ts +30 -24
- package/dist/types/packages/core/src/mcp/factories/index.d.ts +1 -0
- package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +46 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +2 -1
- package/dist/types/packages/core/src/platform/playwright/capability.d.ts +1 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +65 -5
- package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +4 -0
- package/dist/types/packages/core/src/platform/playwright/transport-probe.d.ts +13 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +29 -3
- package/dist/types/packages/core/src/query/active-rooms.d.ts +2 -0
- package/dist/types/packages/core/src/query/public-runtime.d.ts +1 -0
- package/dist/types/packages/core/src/query/state.d.ts +13 -0
- package/dist/types/packages/core/src/query/types.d.ts +2 -1
- package/dist/types/packages/core/src/runtime/playwright-turn-abort.d.ts +12 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export {
|
|
2
|
+
isBrowserJanitorOwner,
|
|
3
|
+
selectOrphanBrowserPids,
|
|
4
|
+
} from "./browser-processes";
|
|
5
|
+
export {
|
|
6
|
+
closeBrowserOwnerTabs,
|
|
7
|
+
configurePlaywrightManagerHost,
|
|
8
|
+
ensureBrowserProfile,
|
|
9
|
+
ensurePlaywright,
|
|
10
|
+
getPlaywrightCapability,
|
|
11
|
+
getPlaywrightManagerHost,
|
|
12
|
+
killAllPlaywright,
|
|
13
|
+
makeBrowserProfileInstanceKey,
|
|
14
|
+
makeInstanceKey,
|
|
15
|
+
onPlaywrightFailure,
|
|
16
|
+
type PlaywrightChildEnvironmentContext,
|
|
17
|
+
type PlaywrightFailure,
|
|
18
|
+
type PlaywrightMaintenanceControl,
|
|
19
|
+
type PlaywrightManagerHost,
|
|
20
|
+
type PlaywrightProfileBinding,
|
|
21
|
+
pinPlaywrightInstance,
|
|
22
|
+
reapPlaywrightOrphans,
|
|
23
|
+
resetPlaywrightManagerHost,
|
|
24
|
+
resolvePlaywrightCapabilityOwner,
|
|
25
|
+
resolvePlaywrightProfileBinding,
|
|
26
|
+
resolvePlaywrightTopicBinding,
|
|
27
|
+
resolveTopicProfileDir,
|
|
28
|
+
stopPlaywrightInstance,
|
|
29
|
+
stopPlaywrightProfile,
|
|
30
|
+
unpinPlaywrightInstance,
|
|
31
|
+
withPlaywrightInstanceMaintenance,
|
|
32
|
+
withPlaywrightProfileMaintenance,
|
|
33
|
+
} from "./manager";
|
|
34
|
+
export {
|
|
35
|
+
browserProcessMatchesExpectedProfile,
|
|
36
|
+
extractUserDataDirArg,
|
|
37
|
+
selectIdleEvictionKey,
|
|
38
|
+
selectReusablePort,
|
|
39
|
+
waitForChildProcessExit,
|
|
40
|
+
waitForChildProcessSpawnError,
|
|
41
|
+
} from "./manager-utils";
|
|
42
|
+
export {
|
|
43
|
+
probeMcpTransport,
|
|
44
|
+
probePlaywrightMcpTransports,
|
|
45
|
+
} from "./transport-probe";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import type { FetchLike, Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
5
|
+
import { browserOwnerCapability } from "#platform/playwright/capability";
|
|
6
|
+
|
|
7
|
+
const PROBE_OWNER = "__negotium_transport_probe__";
|
|
8
|
+
const PROBE_TIMEOUT_MS = 3000;
|
|
9
|
+
|
|
10
|
+
interface ProbeTransportOptions {
|
|
11
|
+
terminate?: () => Promise<void>;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function probeMcpTransport(
|
|
16
|
+
transport: Transport,
|
|
17
|
+
options: ProbeTransportOptions = {},
|
|
18
|
+
): Promise<boolean> {
|
|
19
|
+
const client = new Client({ name: "negotium-browser-healthcheck", version: "1.0.0" });
|
|
20
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const tools = await Promise.race([
|
|
24
|
+
(async () => {
|
|
25
|
+
await client.connect(transport);
|
|
26
|
+
const result = await client.listTools();
|
|
27
|
+
if (result.tools.length === 0) throw new Error("browser MCP returned no tools");
|
|
28
|
+
await options.terminate?.();
|
|
29
|
+
return result.tools;
|
|
30
|
+
})(),
|
|
31
|
+
new Promise<never>((_, reject) => {
|
|
32
|
+
timeout = setTimeout(
|
|
33
|
+
() => reject(new Error("browser MCP transport probe timed out")),
|
|
34
|
+
options.timeoutMs ?? PROBE_TIMEOUT_MS,
|
|
35
|
+
);
|
|
36
|
+
timeout.unref?.();
|
|
37
|
+
}),
|
|
38
|
+
]);
|
|
39
|
+
return tools.length > 0;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
} finally {
|
|
43
|
+
if (timeout) clearTimeout(timeout);
|
|
44
|
+
try {
|
|
45
|
+
await client.close();
|
|
46
|
+
} catch {
|
|
47
|
+
// The probe already failed; close is best-effort for partially opened transports.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Exercise both authenticated transports used by browser-enabled agents.
|
|
54
|
+
* A process-only response or partial SSE handshake is insufficient: each
|
|
55
|
+
* transport must initialize, complete tools/list, and close cleanly.
|
|
56
|
+
*/
|
|
57
|
+
export async function probePlaywrightMcpTransports(
|
|
58
|
+
port: number,
|
|
59
|
+
capability: string,
|
|
60
|
+
fetchImpl: FetchLike = fetch as FetchLike,
|
|
61
|
+
): Promise<boolean> {
|
|
62
|
+
const query = new URLSearchParams({ owner: PROBE_OWNER });
|
|
63
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
64
|
+
const ownerCapability = browserOwnerCapability(capability, PROBE_OWNER);
|
|
65
|
+
const capabilityHeaders = { "X-Browser-Capability": ownerCapability };
|
|
66
|
+
const authenticatedFetch: FetchLike = (input, init) => {
|
|
67
|
+
const headers = new Headers(init?.headers);
|
|
68
|
+
headers.set("X-Browser-Capability", ownerCapability);
|
|
69
|
+
return fetchImpl(input, { ...init, headers });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const sseTransport = new SSEClientTransport(new URL(`${baseUrl}/sse?${query}`), {
|
|
73
|
+
fetch: authenticatedFetch,
|
|
74
|
+
eventSourceInit: { fetch: authenticatedFetch },
|
|
75
|
+
requestInit: { headers: capabilityHeaders },
|
|
76
|
+
});
|
|
77
|
+
if (!(await probeMcpTransport(sseTransport))) return false;
|
|
78
|
+
|
|
79
|
+
const streamableTransport = new StreamableHTTPClientTransport(
|
|
80
|
+
new URL(`${baseUrl}/mcp?${query}`),
|
|
81
|
+
{
|
|
82
|
+
fetch: authenticatedFetch,
|
|
83
|
+
requestInit: { headers: capabilityHeaders },
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
return probeMcpTransport(streamableTransport, {
|
|
87
|
+
terminate: () => streamableTransport.terminateSession(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -175,6 +175,44 @@ export interface SessionSystemPromptOpts {
|
|
|
175
175
|
fileDeliveryTools?: boolean;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
export type SessionPromptKind = "topic" | "channel" | "manager";
|
|
179
|
+
export type PromptSectionSlot =
|
|
180
|
+
| "after-runtime-tools"
|
|
181
|
+
| "after-shared-tasks"
|
|
182
|
+
| "before-session-communication"
|
|
183
|
+
| "after-session-communication"
|
|
184
|
+
| "before-topic-configuration"
|
|
185
|
+
| "after-topic-configuration"
|
|
186
|
+
| "after-system-prompt";
|
|
187
|
+
|
|
188
|
+
export interface PromptSectionContext extends SessionSystemPromptOpts {
|
|
189
|
+
sessionKind: SessionPromptKind;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface PromptExtraSection {
|
|
193
|
+
id: string;
|
|
194
|
+
slot: PromptSectionSlot;
|
|
195
|
+
order?: number;
|
|
196
|
+
render(context: PromptSectionContext): string | null | undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface PromptTemplateRequest {
|
|
200
|
+
kind: "topic-system" | "channel-system" | "manager-system" | "visual-design";
|
|
201
|
+
filename: string;
|
|
202
|
+
fallback: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface PromptBuilderHost {
|
|
206
|
+
readonly loadTemplate?: (request: PromptTemplateRequest) => string | null | undefined;
|
|
207
|
+
readonly extraSections?: readonly PromptExtraSection[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface PromptBuilders {
|
|
211
|
+
buildTopicSystemPrompt(opts: SessionSystemPromptOpts): string;
|
|
212
|
+
buildChannelSystemPrompt(opts: SessionSystemPromptOpts): string;
|
|
213
|
+
buildManagerSystemPrompt(opts: SessionSystemPromptOpts): string;
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
interface RuntimeToolSectionOpts {
|
|
179
217
|
agentKind: AgentKind;
|
|
180
218
|
canSpawnSubagents?: boolean;
|
|
@@ -187,7 +225,15 @@ interface RuntimeToolSectionOpts {
|
|
|
187
225
|
subagentReportMode?: SubagentReportMode;
|
|
188
226
|
}
|
|
189
227
|
|
|
190
|
-
|
|
228
|
+
interface RuntimeToolSectionExtensions {
|
|
229
|
+
render(slot: PromptSectionSlot): string[];
|
|
230
|
+
visualDesignGuide: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildRuntimeToolSection(
|
|
234
|
+
opts: RuntimeToolSectionOpts,
|
|
235
|
+
extensions: RuntimeToolSectionExtensions,
|
|
236
|
+
): string {
|
|
191
237
|
const {
|
|
192
238
|
agentKind,
|
|
193
239
|
canSpawnSubagents = false,
|
|
@@ -282,13 +328,16 @@ function buildRuntimeToolSection(opts: RuntimeToolSectionOpts): string {
|
|
|
282
328
|
'Do not use provider built-in "AskUserQuestion"; it is disabled or unsupported in this headless chat runtime. Use the runtime ask_user_question tool instead.',
|
|
283
329
|
]),
|
|
284
330
|
scheduleSelfToolLine,
|
|
285
|
-
...(visualTools ? ["", visualDesignGuide
|
|
331
|
+
...(visualTools && extensions.visualDesignGuide ? ["", extensions.visualDesignGuide] : []),
|
|
332
|
+
...extensions.render("after-runtime-tools"),
|
|
286
333
|
"",
|
|
287
334
|
"## Shared Tasks",
|
|
288
335
|
taskToolLine,
|
|
289
336
|
"Use this shared task store for plans, task progress, and checklist updates. It is visible across claude/codex/maestro turns and drives the live task panel.",
|
|
290
337
|
nativeTaskPolicyLine,
|
|
338
|
+
...extensions.render("after-shared-tasks"),
|
|
291
339
|
...fileDeliverySection,
|
|
340
|
+
...extensions.render("before-session-communication"),
|
|
292
341
|
"",
|
|
293
342
|
"## Session Communication",
|
|
294
343
|
...(subagentParentTitle
|
|
@@ -313,6 +362,7 @@ function buildRuntimeToolSection(opts: RuntimeToolSectionOpts): string {
|
|
|
313
362
|
: []),
|
|
314
363
|
"Do not use session communication to make another topic perform destructive changes without the user's clear intent.",
|
|
315
364
|
...spawnSubagentSection,
|
|
365
|
+
...extensions.render("after-session-communication"),
|
|
316
366
|
];
|
|
317
367
|
|
|
318
368
|
const modelCatalog = SELECTABLE_MODELS.map(
|
|
@@ -343,69 +393,160 @@ function buildRuntimeToolSection(opts: RuntimeToolSectionOpts): string {
|
|
|
343
393
|
if (agentKind !== "claude") {
|
|
344
394
|
return [
|
|
345
395
|
...shared,
|
|
396
|
+
...extensions.render("before-topic-configuration"),
|
|
346
397
|
...topicConfig,
|
|
398
|
+
...extensions.render("after-topic-configuration"),
|
|
347
399
|
"",
|
|
348
400
|
"## Runtime Tool Limits",
|
|
349
401
|
"If file delivery or topic configuration tools are not present in your available tools for this session, do not claim you used them. Tell the user this session does not expose that in-chat tool action.",
|
|
350
402
|
].join("\n");
|
|
351
403
|
}
|
|
352
404
|
|
|
353
|
-
return [
|
|
405
|
+
return [
|
|
406
|
+
...shared,
|
|
407
|
+
...extensions.render("before-topic-configuration"),
|
|
408
|
+
...topicConfig,
|
|
409
|
+
...extensions.render("after-topic-configuration"),
|
|
410
|
+
].join("\n");
|
|
354
411
|
}
|
|
355
412
|
|
|
356
|
-
export function
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
});
|
|
376
|
-
if (opts.description?.trim()) {
|
|
377
|
-
prompt += `\n\n## Topic-Specific Instructions\n${opts.description.trim()}`;
|
|
413
|
+
export function createPromptBuilders(host: PromptBuilderHost = {}): PromptBuilders {
|
|
414
|
+
const loadTemplate = host.loadTemplate;
|
|
415
|
+
const sections = (host.extraSections ?? []).map((section): PromptExtraSection => {
|
|
416
|
+
const render = section.render;
|
|
417
|
+
const snapshot: PromptExtraSection = {
|
|
418
|
+
id: section.id,
|
|
419
|
+
slot: section.slot,
|
|
420
|
+
...(section.order === undefined ? {} : { order: section.order }),
|
|
421
|
+
render(context) {
|
|
422
|
+
return render.call(snapshot, context);
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
return Object.freeze(snapshot);
|
|
426
|
+
});
|
|
427
|
+
const ids = new Set<string>();
|
|
428
|
+
for (const section of sections) {
|
|
429
|
+
if (!section.id.trim()) throw new Error("prompt extra section id is required");
|
|
430
|
+
if (ids.has(section.id)) throw new Error(`duplicate prompt extra section id: ${section.id}`);
|
|
431
|
+
ids.add(section.id);
|
|
378
432
|
}
|
|
379
|
-
|
|
380
|
-
|
|
433
|
+
sections.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id));
|
|
434
|
+
|
|
435
|
+
const templateCache = new Map<PromptTemplateRequest["kind"], string>();
|
|
436
|
+
const template = (request: PromptTemplateRequest): string => {
|
|
437
|
+
const cached = templateCache.get(request.kind);
|
|
438
|
+
if (cached !== undefined) return cached;
|
|
439
|
+
const loaded = loadTemplate?.(request);
|
|
440
|
+
const value =
|
|
441
|
+
loaded ??
|
|
442
|
+
(request.kind === "topic-system"
|
|
443
|
+
? topicSystemPromptTemplate()
|
|
444
|
+
: request.kind === "channel-system"
|
|
445
|
+
? channelSystemPromptTemplate()
|
|
446
|
+
: request.kind === "manager-system"
|
|
447
|
+
? managerSystemPromptTemplate()
|
|
448
|
+
: visualDesignGuide());
|
|
449
|
+
templateCache.set(request.kind, value);
|
|
450
|
+
return value;
|
|
451
|
+
};
|
|
381
452
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
453
|
+
const build = (
|
|
454
|
+
sessionKind: SessionPromptKind,
|
|
455
|
+
opts: SessionSystemPromptOpts,
|
|
456
|
+
sessionTemplate: string,
|
|
457
|
+
): string => {
|
|
458
|
+
const context = Object.freeze({ ...opts, sessionKind });
|
|
459
|
+
const render = (slot: PromptSectionSlot): string[] =>
|
|
460
|
+
sections
|
|
461
|
+
.filter((section) => section.slot === slot)
|
|
462
|
+
.map((section) => section.render(context)?.trim())
|
|
463
|
+
.filter((section): section is string => Boolean(section))
|
|
464
|
+
.flatMap((section) => ["", section]);
|
|
465
|
+
const uploadsDir = `${opts.workspaceCwd}/attachments`;
|
|
466
|
+
let prompt =
|
|
467
|
+
replaceVars(sessionTemplate, {
|
|
468
|
+
AI_LABEL: opts.aiLabel,
|
|
469
|
+
TOPIC_TITLE: opts.topicTitle,
|
|
470
|
+
WORKSPACE_CWD: opts.workspaceCwd,
|
|
471
|
+
UPLOADS_DIR: uploadsDir,
|
|
472
|
+
}) +
|
|
473
|
+
buildRuntimeToolSection(
|
|
474
|
+
{
|
|
475
|
+
agentKind: opts.agentKind,
|
|
476
|
+
canSpawnSubagents: sessionKind === "channel" ? false : opts.canSpawnSubagents,
|
|
477
|
+
canStageSubagents: sessionKind === "channel" ? false : opts.canStageSubagents,
|
|
478
|
+
visualTools: opts.visualTools,
|
|
479
|
+
fileDeliveryTools: opts.fileDeliveryTools,
|
|
480
|
+
currentModel: opts.currentModel,
|
|
481
|
+
currentEffort: opts.currentEffort,
|
|
482
|
+
subagentParentTitle: opts.subagentParentTitle,
|
|
483
|
+
subagentReportMode: opts.subagentReportMode,
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
render,
|
|
487
|
+
visualDesignGuide: template({
|
|
488
|
+
kind: "visual-design",
|
|
489
|
+
filename: "visual-design.md",
|
|
490
|
+
fallback: "",
|
|
491
|
+
}),
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
if (sessionKind !== "channel" && opts.description?.trim()) {
|
|
495
|
+
prompt += `\n\n## Topic-Specific Instructions\n${opts.description.trim()}`;
|
|
496
|
+
}
|
|
497
|
+
if (sessionKind === "manager") {
|
|
498
|
+
prompt += `\n\n${template({
|
|
499
|
+
kind: "manager-system",
|
|
500
|
+
filename: "manager-system.md",
|
|
501
|
+
fallback: FALLBACK_MANAGER_SYSTEM_PROMPT_TEMPLATE,
|
|
502
|
+
})}`;
|
|
503
|
+
}
|
|
504
|
+
return `${prompt}${render("after-system-prompt").join("\n")}`;
|
|
505
|
+
};
|
|
404
506
|
|
|
405
|
-
|
|
406
|
-
|
|
507
|
+
return Object.freeze({
|
|
508
|
+
buildTopicSystemPrompt(opts: SessionSystemPromptOpts) {
|
|
509
|
+
return build(
|
|
510
|
+
"topic",
|
|
511
|
+
opts,
|
|
512
|
+
template({
|
|
513
|
+
kind: "topic-system",
|
|
514
|
+
filename: "topic-system.md",
|
|
515
|
+
fallback: FALLBACK_TOPIC_SYSTEM_PROMPT_TEMPLATE,
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
518
|
+
},
|
|
519
|
+
buildChannelSystemPrompt(opts: SessionSystemPromptOpts) {
|
|
520
|
+
return build(
|
|
521
|
+
"channel",
|
|
522
|
+
opts,
|
|
523
|
+
template({
|
|
524
|
+
kind: "channel-system",
|
|
525
|
+
filename: "channel-system.md",
|
|
526
|
+
fallback: FALLBACK_CHANNEL_SYSTEM_PROMPT_TEMPLATE,
|
|
527
|
+
}),
|
|
528
|
+
);
|
|
529
|
+
},
|
|
530
|
+
buildManagerSystemPrompt(opts: SessionSystemPromptOpts) {
|
|
531
|
+
return build(
|
|
532
|
+
"manager",
|
|
533
|
+
opts,
|
|
534
|
+
template({
|
|
535
|
+
kind: "topic-system",
|
|
536
|
+
filename: "topic-system.md",
|
|
537
|
+
fallback: FALLBACK_TOPIC_SYSTEM_PROMPT_TEMPLATE,
|
|
538
|
+
}),
|
|
539
|
+
);
|
|
540
|
+
},
|
|
541
|
+
});
|
|
407
542
|
}
|
|
408
543
|
|
|
544
|
+
const defaultPromptBuilders = createPromptBuilders();
|
|
545
|
+
|
|
546
|
+
export const buildTopicSystemPrompt = defaultPromptBuilders.buildTopicSystemPrompt;
|
|
547
|
+
export const buildChannelSystemPrompt = defaultPromptBuilders.buildChannelSystemPrompt;
|
|
548
|
+
export const buildManagerSystemPrompt = defaultPromptBuilders.buildManagerSystemPrompt;
|
|
549
|
+
|
|
409
550
|
export function buildMemoryPromptSection(opts: {
|
|
410
551
|
briefFile: string;
|
|
411
552
|
wikiDir: string;
|
|
@@ -119,6 +119,8 @@ export interface RoomQueryControl {
|
|
|
119
119
|
sessionId?: string | null;
|
|
120
120
|
abortController: AbortController;
|
|
121
121
|
abortReason: AbortReason;
|
|
122
|
+
/** Specific failure surfaced when infrastructure aborts the turn. */
|
|
123
|
+
abortError?: string;
|
|
122
124
|
/** Epoch ms when this turn claimed the room (stale-abort guard). */
|
|
123
125
|
startedAt?: number;
|
|
124
126
|
/**
|
|
@@ -17,6 +17,14 @@ export {
|
|
|
17
17
|
type RoomQueryRegistryHost,
|
|
18
18
|
type RuntimeTurnLeaseLike,
|
|
19
19
|
} from "./room-query-registry";
|
|
20
|
+
export {
|
|
21
|
+
clearQueryState,
|
|
22
|
+
createQueryStateStore,
|
|
23
|
+
type QueryStateStore,
|
|
24
|
+
type QueryStateStoreLogger,
|
|
25
|
+
type QueryStateStoreOptions,
|
|
26
|
+
writeQueryState,
|
|
27
|
+
} from "./state";
|
|
20
28
|
export {
|
|
21
29
|
AbortReason,
|
|
22
30
|
type HandleAgentQueryOutcome,
|
|
@@ -7,38 +7,86 @@ import type { QueryState } from "#types";
|
|
|
7
7
|
|
|
8
8
|
type QueryStateUserId = number | string;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
export interface QueryStateStoreLogger {
|
|
11
|
+
warn(context: Record<string, unknown>, message: string): void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
export interface QueryStateStoreOptions {
|
|
15
|
+
usersLogDir: string;
|
|
16
|
+
logger?: QueryStateStoreLogger;
|
|
17
|
+
sanitizeTopicId?: (topicId: string) => string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
userId: QueryStateUserId,
|
|
20
|
-
|
|
21
|
-
): string | null {
|
|
22
|
-
if (!topicName || topicName === "." || topicName === ".." || basename(topicName) !== topicName) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
return join(queryStateDirPath(userId), `${topicName}.json`);
|
|
20
|
+
export interface QueryStateStore {
|
|
21
|
+
write(userId: QueryStateUserId, topicId: string, topicName: string, task?: string): void;
|
|
22
|
+
clear(userId: QueryStateUserId, topicId: string, legacyTopicName?: string): void;
|
|
26
23
|
}
|
|
27
24
|
|
|
25
|
+
export function createQueryStateStore(options: QueryStateStoreOptions): QueryStateStore {
|
|
26
|
+
const sanitize = options.sanitizeTopicId ?? sanitizeId;
|
|
27
|
+
const queryStateDirPath = (userId: QueryStateUserId): string =>
|
|
28
|
+
join(options.usersLogDir, String(userId), "active-queries");
|
|
29
|
+
const queryStateFile = (userId: QueryStateUserId, topicId: string): string =>
|
|
30
|
+
join(queryStateDirPath(userId), `${sanitize(topicId)}.json`);
|
|
31
|
+
const legacyQueryStateFile = (
|
|
32
|
+
userId: QueryStateUserId,
|
|
33
|
+
topicName: string | undefined,
|
|
34
|
+
): string | null => {
|
|
35
|
+
if (
|
|
36
|
+
!topicName ||
|
|
37
|
+
topicName === "." ||
|
|
38
|
+
topicName === ".." ||
|
|
39
|
+
basename(topicName) !== topicName
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return join(queryStateDirPath(userId), `${topicName}.json`);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
write(userId, topicId, topicName, task) {
|
|
48
|
+
const dir = queryStateDirPath(userId);
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
const state: QueryState = { topicId, topicName, since: new Date().toISOString() };
|
|
51
|
+
if (task) state.task = [...task.replace(/\n+/g, " ").trim()].slice(0, 100).join("");
|
|
52
|
+
const target = queryStateFile(userId, topicId);
|
|
53
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
54
|
+
writeFileSync(tmp, JSON.stringify(state));
|
|
55
|
+
renameSync(tmp, target);
|
|
56
|
+
},
|
|
57
|
+
clear(userId, topicId, legacyTopicName) {
|
|
58
|
+
const paths = [
|
|
59
|
+
queryStateFile(userId, topicId),
|
|
60
|
+
legacyQueryStateFile(userId, legacyTopicName),
|
|
61
|
+
].filter((path): path is string => Boolean(path));
|
|
62
|
+
for (const path of new Set(paths)) {
|
|
63
|
+
try {
|
|
64
|
+
unlinkSync(path);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
67
|
+
options.logger?.warn(
|
|
68
|
+
{ err: error, userId, topicId, path },
|
|
69
|
+
"Failed to clear query state file",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const defaultQueryStateStore = createQueryStateStore({
|
|
79
|
+
usersLogDir: USERS_LOG_DIR,
|
|
80
|
+
logger,
|
|
81
|
+
});
|
|
82
|
+
|
|
28
83
|
export function writeQueryState(
|
|
29
84
|
userId: QueryStateUserId,
|
|
30
85
|
topicId: string,
|
|
31
86
|
topicName: string,
|
|
32
87
|
task?: string,
|
|
33
88
|
) {
|
|
34
|
-
|
|
35
|
-
mkdirSync(dir, { recursive: true });
|
|
36
|
-
const state: QueryState = { topicId, topicName, since: new Date().toISOString() };
|
|
37
|
-
if (task) state.task = [...task.replace(/\n+/g, " ").trim()].slice(0, 100).join("");
|
|
38
|
-
const target = queryStateFile(userId, topicId);
|
|
39
|
-
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
40
|
-
writeFileSync(tmp, JSON.stringify(state));
|
|
41
|
-
renameSync(tmp, target);
|
|
89
|
+
defaultQueryStateStore.write(userId, topicId, topicName, task);
|
|
42
90
|
}
|
|
43
91
|
|
|
44
92
|
export function clearQueryState(
|
|
@@ -46,17 +94,5 @@ export function clearQueryState(
|
|
|
46
94
|
topicId: string,
|
|
47
95
|
legacyTopicName?: string,
|
|
48
96
|
) {
|
|
49
|
-
|
|
50
|
-
queryStateFile(userId, topicId),
|
|
51
|
-
legacyQueryStateFile(userId, legacyTopicName),
|
|
52
|
-
].filter((path): path is string => Boolean(path));
|
|
53
|
-
for (const path of new Set(paths)) {
|
|
54
|
-
try {
|
|
55
|
-
unlinkSync(path);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
58
|
-
logger.warn({ err: e, userId, topicId, path }, "Failed to clear query state file");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
97
|
+
defaultQueryStateStore.clear(userId, topicId, legacyTopicName);
|
|
62
98
|
}
|
|
@@ -80,4 +80,5 @@ export enum AbortReason {
|
|
|
80
80
|
None = "none",
|
|
81
81
|
Internal = "internal", // replaced by a newer query on the same topic
|
|
82
82
|
External = "external", // /abort command or abort_session via session-inbox
|
|
83
|
+
Infrastructure = "infrastructure", // required runtime dependency exited unexpectedly
|
|
83
84
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { logger } from "#platform/logger";
|
|
2
|
+
import { onPlaywrightFailure, type PlaywrightFailure } from "#platform/playwright/manager";
|
|
3
|
+
import { AbortReason } from "#query/types";
|
|
4
|
+
|
|
5
|
+
export interface PlaywrightTurnControl {
|
|
6
|
+
topicId: string;
|
|
7
|
+
queryId: string;
|
|
8
|
+
abortController: AbortController;
|
|
9
|
+
abortReason: AbortReason;
|
|
10
|
+
abortError?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const turnsByInstance = new Map<string, Set<PlaywrightTurnControl>>();
|
|
14
|
+
|
|
15
|
+
export function trackPlaywrightTurn(instanceKey: string, control: PlaywrightTurnControl): void {
|
|
16
|
+
const turns = turnsByInstance.get(instanceKey) ?? new Set<PlaywrightTurnControl>();
|
|
17
|
+
turns.add(control);
|
|
18
|
+
turnsByInstance.set(instanceKey, turns);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function untrackPlaywrightTurn(instanceKey: string, control: PlaywrightTurnControl): void {
|
|
22
|
+
const turns = turnsByInstance.get(instanceKey);
|
|
23
|
+
if (!turns) return;
|
|
24
|
+
turns.delete(control);
|
|
25
|
+
if (turns.size === 0) turnsByInstance.delete(instanceKey);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function failureMessage(failure: PlaywrightFailure): string {
|
|
29
|
+
if (failure.reason === "unhealthy") {
|
|
30
|
+
return "Browser MCP transport became unresponsive and was restarted";
|
|
31
|
+
}
|
|
32
|
+
if (failure.reason === "process-error") {
|
|
33
|
+
return `Browser MCP process failed${failure.error ? `: ${failure.error}` : ""}`;
|
|
34
|
+
}
|
|
35
|
+
return (
|
|
36
|
+
"Browser MCP exited unexpectedly" +
|
|
37
|
+
(failure.code === null || failure.code === undefined ? "" : ` (exitCode=${failure.code})`)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function abortPlaywrightTurns(instanceKey: string, failure: PlaywrightFailure): number {
|
|
42
|
+
const turns = turnsByInstance.get(instanceKey);
|
|
43
|
+
if (!turns) return 0;
|
|
44
|
+
|
|
45
|
+
const error = failureMessage(failure);
|
|
46
|
+
let aborted = 0;
|
|
47
|
+
for (const control of turns) {
|
|
48
|
+
if (control.abortController.signal.aborted) continue;
|
|
49
|
+
control.abortReason = AbortReason.Infrastructure;
|
|
50
|
+
control.abortError = error;
|
|
51
|
+
control.abortController.abort(new Error(error));
|
|
52
|
+
aborted++;
|
|
53
|
+
}
|
|
54
|
+
turnsByInstance.delete(instanceKey);
|
|
55
|
+
logger.warn({ instanceKey, failure, aborted }, "Aborted turns after browser MCP failure");
|
|
56
|
+
return aborted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onPlaywrightFailure(abortPlaywrightTurns);
|