@foldrun/core 2.4.0 → 2.5.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/dist/src/agent-activity.d.ts +5 -0
- package/dist/src/agent-activity.js +31 -0
- package/dist/src/agent-import.js +1 -1
- package/dist/src/agent-tools.js +1 -1
- package/dist/src/agents-md.js +1 -1
- package/dist/src/alerts.d.ts +60 -0
- package/dist/src/alerts.js +161 -0
- package/dist/src/browser-pod.js +4 -0
- package/dist/src/capture.d.ts +23 -0
- package/dist/src/capture.js +80 -0
- package/dist/src/compare.d.ts +57 -0
- package/dist/src/compare.js +116 -0
- package/dist/src/completions.js +1 -0
- package/dist/src/deploy.d.ts +36 -1
- package/dist/src/deploy.js +170 -11
- package/dist/src/egress.d.ts +16 -1
- package/dist/src/evals.d.ts +5 -1
- package/dist/src/evals.js +20 -6
- package/dist/src/frontmatter-fields.js +9 -0
- package/dist/src/frontmatter-schema.js +3 -0
- package/dist/src/history.d.ts +4 -0
- package/dist/src/history.js +15 -0
- package/dist/src/hooks.js +1 -1
- package/dist/src/insights.d.ts +30 -0
- package/dist/src/insights.js +152 -0
- package/dist/src/issues.d.ts +140 -0
- package/dist/src/issues.js +302 -0
- package/dist/src/library.js +1 -1
- package/dist/src/limits.d.ts +8 -0
- package/dist/src/limits.js +36 -5
- package/dist/src/linkable.js +1 -1
- package/dist/src/matter.d.ts +8 -0
- package/dist/src/matter.js +28 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/network.d.ts +12 -0
- package/dist/src/network.js +25 -1
- package/dist/src/notify.d.ts +16 -0
- package/dist/src/notify.js +17 -7
- package/dist/src/okf.js +1 -1
- package/dist/src/online-checks.d.ts +76 -0
- package/dist/src/online-checks.js +432 -0
- package/dist/src/operator.d.ts +277 -0
- package/dist/src/operator.js +176 -2
- package/dist/src/otel-config.d.ts +18 -0
- package/dist/src/otel-config.js +69 -0
- package/dist/src/otel.d.ts +38 -0
- package/dist/src/otel.js +85 -0
- package/dist/src/outcome.d.ts +40 -0
- package/dist/src/outcome.js +77 -0
- package/dist/src/package.js +1 -1
- package/dist/src/platform.d.ts +1 -1
- package/dist/src/prompt-layers.d.ts +30 -0
- package/dist/src/prompt-layers.js +77 -0
- package/dist/src/provider-health.js +1 -1
- package/dist/src/providers.d.ts +5 -0
- package/dist/src/providers.js +2 -2
- package/dist/src/run-container.d.ts +16 -2
- package/dist/src/run-container.js +57 -5
- package/dist/src/runner.d.ts +53 -2
- package/dist/src/runner.js +356 -70
- package/dist/src/runs-filter.d.ts +48 -0
- package/dist/src/runs-filter.js +73 -0
- package/dist/src/scan.d.ts +19 -1
- package/dist/src/scan.js +60 -7
- package/dist/src/starter.js +4 -2
- package/dist/src/step-exec.d.ts +29 -1
- package/dist/src/step-exec.js +106 -6
- package/dist/src/store.d.ts +130 -3
- package/dist/src/store.js +145 -6
- package/dist/src/tokens.d.ts +38 -0
- package/dist/src/tokens.js +57 -0
- package/dist/src/tool-names.js +1 -1
- package/dist/src/tool-programs.js +1 -1
- package/dist/src/trace-tree.d.ts +40 -0
- package/dist/src/trace-tree.js +156 -0
- package/dist/src/translator.d.ts +8 -0
- package/dist/src/translator.js +7 -2
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/workspace-check.d.ts +18 -0
- package/dist/src/workspace-check.js +34 -0
- package/package.json +2 -2
- package/src/agent-activity.ts +32 -0
- package/src/agent-import.ts +1 -1
- package/src/agent-tools.ts +1 -1
- package/src/agents-md.ts +1 -1
- package/src/alerts.ts +190 -0
- package/src/browser-pod.ts +3 -0
- package/src/capture.ts +88 -0
- package/src/compare.ts +146 -0
- package/src/completions.ts +1 -0
- package/src/deploy.ts +184 -10
- package/src/egress.ts +12 -2
- package/src/evals.ts +19 -6
- package/src/frontmatter-fields.ts +9 -0
- package/src/frontmatter-schema.ts +3 -0
- package/src/history.ts +13 -0
- package/src/hooks.ts +1 -1
- package/src/insights.ts +174 -0
- package/src/issues.ts +396 -0
- package/src/library.ts +1 -1
- package/src/limits.ts +38 -5
- package/src/linkable.ts +1 -1
- package/src/matter.ts +33 -0
- package/src/model-credential.ts +45 -0
- package/src/network.ts +25 -1
- package/src/notify.ts +22 -7
- package/src/okf.ts +1 -1
- package/src/online-checks.ts +450 -0
- package/src/operator.ts +213 -3
- package/src/otel-config.ts +67 -0
- package/src/otel.ts +100 -0
- package/src/outcome.ts +90 -0
- package/src/package.ts +1 -1
- package/src/platform.ts +1 -1
- package/src/prompt-layers.ts +90 -0
- package/src/provider-health.ts +1 -1
- package/src/providers.ts +7 -2
- package/src/run-container.ts +66 -7
- package/src/runner.ts +363 -46
- package/src/runs-filter.ts +88 -0
- package/src/scan.ts +68 -7
- package/src/starter.ts +4 -2
- package/src/step-exec.ts +147 -9
- package/src/store.ts +254 -8
- package/src/tokens.ts +68 -0
- package/src/tool-names.ts +1 -1
- package/src/tool-programs.ts +1 -1
- package/src/trace-tree.ts +187 -0
- package/src/translator.ts +13 -2
- package/src/trigger-log.ts +1 -0
- package/src/workspace-check.ts +41 -0
package/dist/src/operator.d.ts
CHANGED
|
@@ -19,7 +19,42 @@ export type OperatorEvent = {
|
|
|
19
19
|
text: string;
|
|
20
20
|
by?: string | null;
|
|
21
21
|
at: string;
|
|
22
|
+
}
|
|
23
|
+
/** A colleague running beside this step sent it a message (tools: [peers]). */
|
|
24
|
+
| {
|
|
25
|
+
kind: "peer";
|
|
26
|
+
from: string;
|
|
27
|
+
text: string;
|
|
28
|
+
at: string;
|
|
29
|
+
}
|
|
30
|
+
/** This step sent a colleague a message. `to` is the agents it reached. */
|
|
31
|
+
| {
|
|
32
|
+
kind: "told";
|
|
33
|
+
to: string[];
|
|
34
|
+
text: string;
|
|
35
|
+
at: string;
|
|
22
36
|
};
|
|
37
|
+
/** A step running in the same parallel group, as its siblings see it. */
|
|
38
|
+
export interface PeerInfo {
|
|
39
|
+
/** The sibling's agent name — what `peers_message` is addressed to. */
|
|
40
|
+
agent: string;
|
|
41
|
+
/** Its index in run.steps: which inbox a message to it goes to. */
|
|
42
|
+
step: number;
|
|
43
|
+
/** The first line of its instruction, so a sibling knows what it is on. */
|
|
44
|
+
instruction: string;
|
|
45
|
+
}
|
|
46
|
+
/** A sibling's address: `agent#step`, the step as the flow numbers it
|
|
47
|
+
* (1-based). A fan-out's instances share an agent name, so the bare name
|
|
48
|
+
* reaches every instance and the address reaches one. */
|
|
49
|
+
export declare const peerAddress: (p: {
|
|
50
|
+
agent: string;
|
|
51
|
+
step: number;
|
|
52
|
+
}) => string;
|
|
53
|
+
/** Does `to` name this sibling? `all`, its agent name, or its address. */
|
|
54
|
+
export declare const peerMatches: (p: PeerInfo, to: string) => boolean;
|
|
55
|
+
/** `by` on an inbox message from a colleague, not a person: `peer:<address>`. */
|
|
56
|
+
export declare const PEER_BY_PREFIX = "peer:";
|
|
57
|
+
export declare const peerOf: (by: string | null | undefined) => string | null;
|
|
23
58
|
export interface OperatorMessage {
|
|
24
59
|
text: string;
|
|
25
60
|
by?: string | null;
|
|
@@ -40,6 +75,15 @@ export interface OperatorChannel {
|
|
|
40
75
|
/** Nobody will read this question's answer now (the wait timed out):
|
|
41
76
|
* stop offering it to people. Best effort; optional for fakes. */
|
|
42
77
|
close?(id: string): Promise<void>;
|
|
78
|
+
/** tools: [peers] — the siblings this step may message, as the lease
|
|
79
|
+
* carries them; empty when the step runs alone. Optional: an older
|
|
80
|
+
* proxy, or a fake, has no colleagues to offer. */
|
|
81
|
+
peers?(): Promise<PeerInfo[]>;
|
|
82
|
+
/** Leave `text` in the inbox of the sibling named `to` (or of every
|
|
83
|
+
* sibling, `"all"`). Resolves with the agents it reached. */
|
|
84
|
+
tell?(to: string, text: string): Promise<{
|
|
85
|
+
to: string[];
|
|
86
|
+
}>;
|
|
43
87
|
}
|
|
44
88
|
export declare const QUESTION_MAX = 2000;
|
|
45
89
|
export declare const OPTION_MAX = 200;
|
|
@@ -57,6 +101,21 @@ export declare function operatorEndpoint(egress: string | undefined | null): {
|
|
|
57
101
|
} | null;
|
|
58
102
|
/** The channel over the egress proxy. `fetchImpl` is for tests. */
|
|
59
103
|
export declare function httpChannel(egress: string | undefined | null, fetchImpl?: typeof fetch): OperatorChannel | null;
|
|
104
|
+
/**
|
|
105
|
+
* The colleagues' line for steps that run in this process (`foldrun run`
|
|
106
|
+
* on a laptop, a host without a proxy): one bus per run, an inbox per step,
|
|
107
|
+
* the same shape the proxy serves. A person has no door here — nobody can
|
|
108
|
+
* write in from outside — but siblings can still reach each other.
|
|
109
|
+
*/
|
|
110
|
+
export declare function localPeerBus(): {
|
|
111
|
+
/** The step is over (however it ended): nobody can reach it now. */
|
|
112
|
+
leave(step: number): void;
|
|
113
|
+
/** The channel for one step: who its siblings are, and its own inbox. */
|
|
114
|
+
channelFor(self: {
|
|
115
|
+
agent: string;
|
|
116
|
+
step: number;
|
|
117
|
+
}, siblings: PeerInfo[]): OperatorChannel;
|
|
118
|
+
};
|
|
60
119
|
/** `ask: {timeout: 2h}` in an agent's frontmatter → seconds, clamped. */
|
|
61
120
|
export declare function askTimeoutSec(front: Record<string, unknown>): number;
|
|
62
121
|
/** "90s", "30m", "2h", "1d" or a number of seconds; null when unreadable. */
|
|
@@ -179,6 +238,224 @@ export declare function buildAskTool(opts: {
|
|
|
179
238
|
};
|
|
180
239
|
/** The granted name of the tool — the same on every path. */
|
|
181
240
|
export declare const ASK_TOOL = "mcp__foldrun_ask__ask_person";
|
|
241
|
+
/** The granted names of the colleagues' tools — the same on every path. */
|
|
242
|
+
export declare const PEER_TOOLS: string[];
|
|
243
|
+
export declare const PEER_TEXT_MAX = 4000;
|
|
244
|
+
/** The in-sandbox `peers_list` and `peers_message` tools (tools: [peers]).
|
|
245
|
+
* `siblings` is what the runner knew at launch: the other steps of this
|
|
246
|
+
* step's parallel group. The channel delivers; with none (no proxy and no
|
|
247
|
+
* local bus) the tools say so rather than pretend. */
|
|
248
|
+
export declare function buildPeerTools(opts: {
|
|
249
|
+
channel: OperatorChannel | null;
|
|
250
|
+
self: string;
|
|
251
|
+
siblings: PeerInfo[];
|
|
252
|
+
emit: Emit;
|
|
253
|
+
now?: () => number;
|
|
254
|
+
}): {
|
|
255
|
+
server: import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
256
|
+
/** The tools' own functions, for tests. */
|
|
257
|
+
list: () => Promise<{
|
|
258
|
+
[x: string]: unknown;
|
|
259
|
+
_meta?: {
|
|
260
|
+
[x: string]: unknown;
|
|
261
|
+
progressToken?: string | number | undefined;
|
|
262
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
263
|
+
taskId: string;
|
|
264
|
+
} | undefined;
|
|
265
|
+
} | undefined;
|
|
266
|
+
content: ({
|
|
267
|
+
type: "text";
|
|
268
|
+
text: string;
|
|
269
|
+
annotations?: {
|
|
270
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
271
|
+
priority?: number | undefined;
|
|
272
|
+
lastModified?: string | undefined;
|
|
273
|
+
} | undefined;
|
|
274
|
+
_meta?: {
|
|
275
|
+
[x: string]: unknown;
|
|
276
|
+
} | undefined;
|
|
277
|
+
} | {
|
|
278
|
+
type: "image";
|
|
279
|
+
data: string;
|
|
280
|
+
mimeType: string;
|
|
281
|
+
annotations?: {
|
|
282
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
283
|
+
priority?: number | undefined;
|
|
284
|
+
lastModified?: string | undefined;
|
|
285
|
+
} | undefined;
|
|
286
|
+
_meta?: {
|
|
287
|
+
[x: string]: unknown;
|
|
288
|
+
} | undefined;
|
|
289
|
+
} | {
|
|
290
|
+
type: "audio";
|
|
291
|
+
data: string;
|
|
292
|
+
mimeType: string;
|
|
293
|
+
annotations?: {
|
|
294
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
295
|
+
priority?: number | undefined;
|
|
296
|
+
lastModified?: string | undefined;
|
|
297
|
+
} | undefined;
|
|
298
|
+
_meta?: {
|
|
299
|
+
[x: string]: unknown;
|
|
300
|
+
} | undefined;
|
|
301
|
+
} | {
|
|
302
|
+
uri: string;
|
|
303
|
+
description?: string | undefined;
|
|
304
|
+
mimeType?: string | undefined;
|
|
305
|
+
size?: number | undefined;
|
|
306
|
+
annotations?: {
|
|
307
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
308
|
+
priority?: number | undefined;
|
|
309
|
+
lastModified?: string | undefined;
|
|
310
|
+
} | undefined;
|
|
311
|
+
_meta?: {
|
|
312
|
+
[x: string]: unknown;
|
|
313
|
+
} | undefined;
|
|
314
|
+
icons?: {
|
|
315
|
+
src: string;
|
|
316
|
+
mimeType?: string | undefined;
|
|
317
|
+
sizes?: string[] | undefined;
|
|
318
|
+
theme?: "dark" | "light" | undefined;
|
|
319
|
+
}[] | undefined;
|
|
320
|
+
name: string;
|
|
321
|
+
title?: string | undefined;
|
|
322
|
+
type: "resource_link";
|
|
323
|
+
} | {
|
|
324
|
+
type: "resource";
|
|
325
|
+
resource: {
|
|
326
|
+
uri: string;
|
|
327
|
+
mimeType?: string | undefined;
|
|
328
|
+
_meta?: {
|
|
329
|
+
[x: string]: unknown;
|
|
330
|
+
} | undefined;
|
|
331
|
+
text: string;
|
|
332
|
+
} | {
|
|
333
|
+
uri: string;
|
|
334
|
+
mimeType?: string | undefined;
|
|
335
|
+
_meta?: {
|
|
336
|
+
[x: string]: unknown;
|
|
337
|
+
} | undefined;
|
|
338
|
+
blob: string;
|
|
339
|
+
};
|
|
340
|
+
annotations?: {
|
|
341
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
342
|
+
priority?: number | undefined;
|
|
343
|
+
lastModified?: string | undefined;
|
|
344
|
+
} | undefined;
|
|
345
|
+
_meta?: {
|
|
346
|
+
[x: string]: unknown;
|
|
347
|
+
} | undefined;
|
|
348
|
+
})[];
|
|
349
|
+
structuredContent?: {
|
|
350
|
+
[x: string]: unknown;
|
|
351
|
+
} | undefined;
|
|
352
|
+
isError?: boolean | undefined;
|
|
353
|
+
}>;
|
|
354
|
+
message: (args: {
|
|
355
|
+
to: string;
|
|
356
|
+
text: string;
|
|
357
|
+
}) => Promise<{
|
|
358
|
+
[x: string]: unknown;
|
|
359
|
+
_meta?: {
|
|
360
|
+
[x: string]: unknown;
|
|
361
|
+
progressToken?: string | number | undefined;
|
|
362
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
363
|
+
taskId: string;
|
|
364
|
+
} | undefined;
|
|
365
|
+
} | undefined;
|
|
366
|
+
content: ({
|
|
367
|
+
type: "text";
|
|
368
|
+
text: string;
|
|
369
|
+
annotations?: {
|
|
370
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
371
|
+
priority?: number | undefined;
|
|
372
|
+
lastModified?: string | undefined;
|
|
373
|
+
} | undefined;
|
|
374
|
+
_meta?: {
|
|
375
|
+
[x: string]: unknown;
|
|
376
|
+
} | undefined;
|
|
377
|
+
} | {
|
|
378
|
+
type: "image";
|
|
379
|
+
data: string;
|
|
380
|
+
mimeType: string;
|
|
381
|
+
annotations?: {
|
|
382
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
383
|
+
priority?: number | undefined;
|
|
384
|
+
lastModified?: string | undefined;
|
|
385
|
+
} | undefined;
|
|
386
|
+
_meta?: {
|
|
387
|
+
[x: string]: unknown;
|
|
388
|
+
} | undefined;
|
|
389
|
+
} | {
|
|
390
|
+
type: "audio";
|
|
391
|
+
data: string;
|
|
392
|
+
mimeType: string;
|
|
393
|
+
annotations?: {
|
|
394
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
395
|
+
priority?: number | undefined;
|
|
396
|
+
lastModified?: string | undefined;
|
|
397
|
+
} | undefined;
|
|
398
|
+
_meta?: {
|
|
399
|
+
[x: string]: unknown;
|
|
400
|
+
} | undefined;
|
|
401
|
+
} | {
|
|
402
|
+
uri: string;
|
|
403
|
+
description?: string | undefined;
|
|
404
|
+
mimeType?: string | undefined;
|
|
405
|
+
size?: number | undefined;
|
|
406
|
+
annotations?: {
|
|
407
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
408
|
+
priority?: number | undefined;
|
|
409
|
+
lastModified?: string | undefined;
|
|
410
|
+
} | undefined;
|
|
411
|
+
_meta?: {
|
|
412
|
+
[x: string]: unknown;
|
|
413
|
+
} | undefined;
|
|
414
|
+
icons?: {
|
|
415
|
+
src: string;
|
|
416
|
+
mimeType?: string | undefined;
|
|
417
|
+
sizes?: string[] | undefined;
|
|
418
|
+
theme?: "dark" | "light" | undefined;
|
|
419
|
+
}[] | undefined;
|
|
420
|
+
name: string;
|
|
421
|
+
title?: string | undefined;
|
|
422
|
+
type: "resource_link";
|
|
423
|
+
} | {
|
|
424
|
+
type: "resource";
|
|
425
|
+
resource: {
|
|
426
|
+
uri: string;
|
|
427
|
+
mimeType?: string | undefined;
|
|
428
|
+
_meta?: {
|
|
429
|
+
[x: string]: unknown;
|
|
430
|
+
} | undefined;
|
|
431
|
+
text: string;
|
|
432
|
+
} | {
|
|
433
|
+
uri: string;
|
|
434
|
+
mimeType?: string | undefined;
|
|
435
|
+
_meta?: {
|
|
436
|
+
[x: string]: unknown;
|
|
437
|
+
} | undefined;
|
|
438
|
+
blob: string;
|
|
439
|
+
};
|
|
440
|
+
annotations?: {
|
|
441
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
442
|
+
priority?: number | undefined;
|
|
443
|
+
lastModified?: string | undefined;
|
|
444
|
+
} | undefined;
|
|
445
|
+
_meta?: {
|
|
446
|
+
[x: string]: unknown;
|
|
447
|
+
} | undefined;
|
|
448
|
+
})[];
|
|
449
|
+
structuredContent?: {
|
|
450
|
+
[x: string]: unknown;
|
|
451
|
+
} | undefined;
|
|
452
|
+
isError?: boolean | undefined;
|
|
453
|
+
}>;
|
|
454
|
+
toolNames: string[];
|
|
455
|
+
promptLines: string[];
|
|
456
|
+
};
|
|
457
|
+
/** The line the system prompt gets for tools: [peers]. */
|
|
458
|
+
export declare function peersPromptLine(siblings: PeerInfo[]): string;
|
|
182
459
|
/** The line the system prompt gets for it. */
|
|
183
460
|
export declare function askPromptLine(timeoutSec: number): string;
|
|
184
461
|
/** Reads the run's inbox after tool calls and turns messages into context
|
package/dist/src/operator.js
CHANGED
|
@@ -20,8 +20,34 @@
|
|
|
20
20
|
//
|
|
21
21
|
// Both leave a trace event with an `operator` field, which the runner turns
|
|
22
22
|
// into step.questions / step.messages on the record.
|
|
23
|
+
//
|
|
24
|
+
// And a third line, between colleagues (20 Oct 2026):
|
|
25
|
+
//
|
|
26
|
+
// tools: [peers] the agent gets `peers_list` and `peers_message` — who
|
|
27
|
+
// else is running in its parallel group right now, and a
|
|
28
|
+
// message to one of them (or all). It lands in the
|
|
29
|
+
// sibling's own inbox, so it reaches that model after its
|
|
30
|
+
// next tool call, the same door a person's message uses,
|
|
31
|
+
// and the sender hears any reply the same way. A step that
|
|
32
|
+
// finds mid-way that it needs something a sibling has —
|
|
33
|
+
// a file name, a decision, a number — asks for it instead
|
|
34
|
+
// of guessing or failing. Nothing waits: the step goes on
|
|
35
|
+
// and the answer, if one comes, arrives as context.
|
|
36
|
+
//
|
|
37
|
+
// Peer messages travel the person's inbox with `by` set to `peer:<agent>`,
|
|
38
|
+
// so the reader can tell a colleague from a person without a second store
|
|
39
|
+
// or a second door on the proxy.
|
|
23
40
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
24
41
|
import { z } from "zod";
|
|
42
|
+
/** A sibling's address: `agent#step`, the step as the flow numbers it
|
|
43
|
+
* (1-based). A fan-out's instances share an agent name, so the bare name
|
|
44
|
+
* reaches every instance and the address reaches one. */
|
|
45
|
+
export const peerAddress = (p) => `${p.agent}#${p.step + 1}`;
|
|
46
|
+
/** Does `to` name this sibling? `all`, its agent name, or its address. */
|
|
47
|
+
export const peerMatches = (p, to) => to === "all" || to === p.agent || to === peerAddress(p);
|
|
48
|
+
/** `by` on an inbox message from a colleague, not a person: `peer:<address>`. */
|
|
49
|
+
export const PEER_BY_PREFIX = "peer:";
|
|
50
|
+
export const peerOf = (by) => typeof by === "string" && by.startsWith(PEER_BY_PREFIX) ? by.slice(PEER_BY_PREFIX.length) : null;
|
|
25
51
|
export const QUESTION_MAX = 2_000;
|
|
26
52
|
export const OPTION_MAX = 200;
|
|
27
53
|
export const OPTIONS_MAX = 10;
|
|
@@ -76,6 +102,68 @@ export function httpChannel(egress, fetchImpl = fetch) {
|
|
|
76
102
|
signal: AbortSignal.timeout(5_000),
|
|
77
103
|
}));
|
|
78
104
|
},
|
|
105
|
+
async peers() {
|
|
106
|
+
const body = await json(await fetchImpl(`${ep.base}/peers/${ep.token}`, { signal: AbortSignal.timeout(5_000) }));
|
|
107
|
+
return Array.isArray(body.peers) ? body.peers : [];
|
|
108
|
+
},
|
|
109
|
+
async tell(to, text) {
|
|
110
|
+
const body = await json(await fetchImpl(`${ep.base}/peers/${ep.token}/message`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "content-type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ to, text }),
|
|
114
|
+
signal: AbortSignal.timeout(10_000),
|
|
115
|
+
}));
|
|
116
|
+
return { to: Array.isArray(body.to) ? body.to : [] };
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The colleagues' line for steps that run in this process (`foldrun run`
|
|
122
|
+
* on a laptop, a host without a proxy): one bus per run, an inbox per step,
|
|
123
|
+
* the same shape the proxy serves. A person has no door here — nobody can
|
|
124
|
+
* write in from outside — but siblings can still reach each other.
|
|
125
|
+
*/
|
|
126
|
+
export function localPeerBus() {
|
|
127
|
+
const inboxes = new Map();
|
|
128
|
+
/** Steps that have ended: off every roster, and a message to one reaches
|
|
129
|
+
* nobody — it would never be read. */
|
|
130
|
+
const gone = new Set();
|
|
131
|
+
const live = (siblings) => siblings.filter((p) => !gone.has(p.step));
|
|
132
|
+
return {
|
|
133
|
+
/** The step is over (however it ended): nobody can reach it now. */
|
|
134
|
+
leave(step) {
|
|
135
|
+
gone.add(step);
|
|
136
|
+
inboxes.delete(step);
|
|
137
|
+
},
|
|
138
|
+
/** The channel for one step: who its siblings are, and its own inbox. */
|
|
139
|
+
channelFor(self, siblings) {
|
|
140
|
+
return {
|
|
141
|
+
async ask() {
|
|
142
|
+
throw new Error("nobody can answer here");
|
|
143
|
+
},
|
|
144
|
+
async poll() {
|
|
145
|
+
return null;
|
|
146
|
+
},
|
|
147
|
+
async inbox() {
|
|
148
|
+
const mine = inboxes.get(self.step) ?? [];
|
|
149
|
+
inboxes.delete(self.step);
|
|
150
|
+
return mine;
|
|
151
|
+
},
|
|
152
|
+
async peers() {
|
|
153
|
+
return live(siblings);
|
|
154
|
+
},
|
|
155
|
+
async tell(to, text) {
|
|
156
|
+
const targets = live(siblings).filter((p) => peerMatches(p, to));
|
|
157
|
+
const at = new Date().toISOString();
|
|
158
|
+
for (const p of targets) {
|
|
159
|
+
const list = inboxes.get(p.step) ?? [];
|
|
160
|
+
list.push({ text: text.slice(0, MESSAGE_MAX), by: PEER_BY_PREFIX + peerAddress(self), at });
|
|
161
|
+
inboxes.set(p.step, list.slice(-50));
|
|
162
|
+
}
|
|
163
|
+
return { to: targets.map(peerAddress) };
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
},
|
|
79
167
|
};
|
|
80
168
|
}
|
|
81
169
|
/** `ask: {timeout: 2h}` in an agent's frontmatter → seconds, clamped. */
|
|
@@ -167,6 +255,71 @@ export function buildAskTool(opts) {
|
|
|
167
255
|
}
|
|
168
256
|
/** The granted name of the tool — the same on every path. */
|
|
169
257
|
export const ASK_TOOL = "mcp__foldrun_ask__ask_person";
|
|
258
|
+
/** The granted names of the colleagues' tools — the same on every path. */
|
|
259
|
+
export const PEER_TOOLS = ["mcp__foldrun_peers__peers_list", "mcp__foldrun_peers__peers_message"];
|
|
260
|
+
export const PEER_TEXT_MAX = MESSAGE_MAX;
|
|
261
|
+
/** The in-sandbox `peers_list` and `peers_message` tools (tools: [peers]).
|
|
262
|
+
* `siblings` is what the runner knew at launch: the other steps of this
|
|
263
|
+
* step's parallel group. The channel delivers; with none (no proxy and no
|
|
264
|
+
* local bus) the tools say so rather than pretend. */
|
|
265
|
+
export function buildPeerTools(opts) {
|
|
266
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
267
|
+
const roster = () => opts.siblings.length
|
|
268
|
+
? opts.siblings.map((p) => `- ${peerAddress(p)}: ${p.instruction}`).join("\n")
|
|
269
|
+
: "Nobody: this step runs alone in its group.";
|
|
270
|
+
const list = tool("peers_list", "Who else is running beside you right now — the other steps of your parallel group, each as agent#step with what it is doing. Message one with peers_message, addressed as agent#step (or the bare agent name, which reaches every instance of that agent).", {}, async () => {
|
|
271
|
+
let peers = opts.siblings;
|
|
272
|
+
try {
|
|
273
|
+
peers = (await opts.channel?.peers?.()) ?? opts.siblings;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
/* the launch-time roster stands */
|
|
277
|
+
}
|
|
278
|
+
opts.siblings = peers;
|
|
279
|
+
return text(`Running beside you in this group:\n${roster()}`);
|
|
280
|
+
});
|
|
281
|
+
const message = tool("peers_message", "Send a short message to a colleague running beside you in this parallel group — to ask for something it has (a file name, a figure, a decision) or to tell it something it needs. It reaches that agent after its next tool call; any reply reaches you the same way, after your next tool call, so carry on working and check what arrives. Not a way to hand over your work.", {
|
|
282
|
+
to: z.string().describe("The colleague's address from peers_list (agent#step), its bare agent name (every instance of it), or \"all\" for every sibling"),
|
|
283
|
+
text: z.string().describe("The complete message — the colleague has not read your context; say who you are, what you need and where to put it"),
|
|
284
|
+
}, async ({ to, text: body }) => {
|
|
285
|
+
const target = String(to).trim();
|
|
286
|
+
const msg = String(body).trim().slice(0, PEER_TEXT_MAX);
|
|
287
|
+
if (!target || !msg)
|
|
288
|
+
return { ...text("peers_message needs `to` and `text`."), isError: true };
|
|
289
|
+
if (!opts.siblings.some((p) => peerMatches(p, target))) {
|
|
290
|
+
return { ...text(`No colleague named "${target}" runs beside you. ${roster()}`), isError: true };
|
|
291
|
+
}
|
|
292
|
+
if (!opts.channel?.tell)
|
|
293
|
+
return { ...text("No line to colleagues here (no proxy): the message was not delivered."), isError: true };
|
|
294
|
+
let reached;
|
|
295
|
+
try {
|
|
296
|
+
({ to: reached } = await opts.channel.tell(target, msg));
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
return { ...text(`Could not deliver: ${err instanceof Error ? err.message : String(err)}`), isError: true };
|
|
300
|
+
}
|
|
301
|
+
const at = new Date(opts.now?.() ?? Date.now()).toISOString();
|
|
302
|
+
opts.emit("info", `told ${reached.join(", ") || "nobody"}: ${msg}`, { operator: { kind: "told", to: reached, text: msg, at } });
|
|
303
|
+
if (!reached.length)
|
|
304
|
+
return text(`Nobody received it: "${target}" is not running beside you now.`);
|
|
305
|
+
return text(`Delivered to ${reached.join(", ")}. It reaches them after their next tool call; a reply reaches you after yours.`);
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
server: createSdkMcpServer({ name: "foldrun_peers", version: "1.0.0", tools: [list, message] }),
|
|
309
|
+
/** The tools' own functions, for tests. */
|
|
310
|
+
list: () => list.handler({}, {}),
|
|
311
|
+
message: (args) => message.handler(args, {}),
|
|
312
|
+
toolNames: PEER_TOOLS,
|
|
313
|
+
promptLines: [peersPromptLine(opts.siblings)],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/** The line the system prompt gets for tools: [peers]. */
|
|
317
|
+
export function peersPromptLine(siblings) {
|
|
318
|
+
const who = siblings.length ? siblings.map((p) => `${peerAddress(p)} (${p.instruction})`).join("; ") : "nobody this run — this step runs alone";
|
|
319
|
+
return (`\`peers_list\` names the colleagues running beside you in this step's parallel group: ${who}. ` +
|
|
320
|
+
`\`peers_message\` sends one of them (or all) a short message; it reaches them after their next tool call, and their reply reaches you after yours, as context. ` +
|
|
321
|
+
`Ask a colleague for what it has before you guess or stop; keep working while you wait, and read what arrives.`);
|
|
322
|
+
}
|
|
170
323
|
/** The line the system prompt gets for it. */
|
|
171
324
|
export function askPromptLine(timeoutSec) {
|
|
172
325
|
return `\`ask_person\` asks the person running this desk and waits for the answer (up to ${fmtWait(timeoutSec)}). Use it for a decision you cannot make safely yourself — not for something you can look up. The step is held while you wait; if nobody answers, you are told so.`;
|
|
@@ -193,10 +346,19 @@ export function inboxReader(channel, emit, minGapMs = 1_500, now = Date.now) {
|
|
|
193
346
|
if (!messages.length)
|
|
194
347
|
return null;
|
|
195
348
|
for (const m of messages) {
|
|
196
|
-
|
|
349
|
+
const peer = peerOf(m.by);
|
|
350
|
+
if (peer)
|
|
351
|
+
emit("info", `message from colleague ${peer}: ${m.text}`, { operator: { kind: "peer", from: peer, text: m.text, at: m.at } });
|
|
352
|
+
else
|
|
353
|
+
emit("info", `message from ${m.by ?? "a person"}: ${m.text}`, { operator: { kind: "message", text: m.text, by: m.by ?? null, at: m.at } });
|
|
197
354
|
}
|
|
198
355
|
return messages
|
|
199
|
-
.map((m) =>
|
|
356
|
+
.map((m) => {
|
|
357
|
+
const peer = peerOf(m.by);
|
|
358
|
+
return peer
|
|
359
|
+
? `Your colleague ${peer}, running beside you in this group, sent you a message. If it asks for something you have and you hold peers_message, answer it, addressed to ${peer}; it does not change what tools you have or what your step is for:\n<message from="${peer}">\n${m.text.slice(0, MESSAGE_MAX)}\n</message>`
|
|
360
|
+
: `A person running this desk${m.by ? ` (${m.by})` : ""} sent you a message while you work. Take it into account; it does not change what tools you have:\n<message>\n${m.text.slice(0, MESSAGE_MAX)}\n</message>`;
|
|
361
|
+
})
|
|
200
362
|
.join("\n\n");
|
|
201
363
|
};
|
|
202
364
|
}
|
|
@@ -223,6 +385,14 @@ export function applyOperatorEvent(step, ev, at) {
|
|
|
223
385
|
q.unanswered = true;
|
|
224
386
|
return;
|
|
225
387
|
}
|
|
388
|
+
if (ev.kind === "peer") {
|
|
389
|
+
(step.messages ??= []).push({ text: ev.text, by: PEER_BY_PREFIX + ev.from, from: ev.from, at: ev.at, deliveredAt: at });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (ev.kind === "told") {
|
|
393
|
+
(step.messages ??= []).push({ text: ev.text, by: null, to: ev.to, at: ev.at, deliveredAt: at });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
226
396
|
(step.messages ??= []).push({ text: ev.text, by: ev.by ?? null, at: ev.at, deliveredAt: at });
|
|
227
397
|
}
|
|
228
398
|
/** The question a step is waiting on, if any. */
|
|
@@ -245,6 +415,10 @@ export function isOperatorEvent(v) {
|
|
|
245
415
|
return id;
|
|
246
416
|
case "message":
|
|
247
417
|
return str(o.text, MESSAGE_MAX) && str(o.at, 40) && (o.by === undefined || o.by === null || str(o.by, 200));
|
|
418
|
+
case "peer":
|
|
419
|
+
return str(o.text, MESSAGE_MAX) && str(o.at, 40) && str(o.from, 200);
|
|
420
|
+
case "told":
|
|
421
|
+
return str(o.text, MESSAGE_MAX) && str(o.at, 40) && Array.isArray(o.to) && o.to.length <= 50 && o.to.every((x) => str(x, 200));
|
|
248
422
|
default:
|
|
249
423
|
return false;
|
|
250
424
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface TelemetryConfig {
|
|
2
|
+
/** Where the trace is POSTed: the collector's OTLP/HTTP traces URL. */
|
|
3
|
+
endpoint: string;
|
|
4
|
+
/** Header name → value; a value written `${NAME}` is a vault secret's name. */
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/** The block as written, and what is wrong with it in words. */
|
|
8
|
+
export declare function parseTelemetry(raw: unknown): {
|
|
9
|
+
config: TelemetryConfig | null;
|
|
10
|
+
problems: string[];
|
|
11
|
+
};
|
|
12
|
+
/** The vault secrets a config's headers name. */
|
|
13
|
+
export declare function telemetrySecretNames(cfg: TelemetryConfig): string[];
|
|
14
|
+
/** Header values with each `${NAME}` filled by `lookup`; the names it could not fill. */
|
|
15
|
+
export declare function telemetryHeaders(cfg: TelemetryConfig, lookup: (name: string) => string | null): {
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
missing: string[];
|
|
18
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// `telemetry:` as written in AGENTS.md — read, checked and its header
|
|
2
|
+
// secrets filled. No node imports: the dashboard's settings form reads it
|
|
3
|
+
// in the browser. The trace itself is otel.ts.
|
|
4
|
+
const SECRET_REF = /^\$\{([A-Z][A-Z0-9_]{1,63})\}$/;
|
|
5
|
+
const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
|
|
6
|
+
/** The block as written, and what is wrong with it in words. */
|
|
7
|
+
export function parseTelemetry(raw) {
|
|
8
|
+
if (raw === undefined || raw === null)
|
|
9
|
+
return { config: null, problems: [] };
|
|
10
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
11
|
+
return { config: null, problems: ["telemetry: is a block — otlp: <https URL of the collector's /v1/traces>, and headers:"] };
|
|
12
|
+
}
|
|
13
|
+
const o = raw;
|
|
14
|
+
const problems = [];
|
|
15
|
+
for (const k of Object.keys(o))
|
|
16
|
+
if (k !== "otlp" && k !== "headers")
|
|
17
|
+
problems.push(`telemetry: ${k} — not a key; the keys are otlp and headers`);
|
|
18
|
+
const endpoint = typeof o.otlp === "string" ? o.otlp.trim() : "";
|
|
19
|
+
let url = null;
|
|
20
|
+
try {
|
|
21
|
+
url = endpoint ? new URL(endpoint) : null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
url = null;
|
|
25
|
+
}
|
|
26
|
+
if (!url || url.protocol !== "https:")
|
|
27
|
+
problems.push(`telemetry: otlp ${endpoint || "(missing)"} — an https:// URL, the collector's OTLP/HTTP traces endpoint (…/v1/traces)`);
|
|
28
|
+
const headers = {};
|
|
29
|
+
if (o.headers !== undefined) {
|
|
30
|
+
if (!o.headers || typeof o.headers !== "object" || Array.isArray(o.headers))
|
|
31
|
+
problems.push("telemetry: headers is a block of name: value");
|
|
32
|
+
else {
|
|
33
|
+
for (const [name, value] of Object.entries(o.headers)) {
|
|
34
|
+
if (!HEADER_NAME.test(name))
|
|
35
|
+
problems.push(`telemetry: header "${name}" — letters, digits and dashes`);
|
|
36
|
+
else if (typeof value !== "string" || !value.trim())
|
|
37
|
+
problems.push(`telemetry: header ${name} — a value, or \${SECRET_NAME} for one in the vault`);
|
|
38
|
+
else if (!SECRET_REF.test(value.trim()) && /token|key|secret|auth/i.test(name)) {
|
|
39
|
+
problems.push(`telemetry: header ${name} is written into the file — put it in the vault (foldrun secrets set NAME) and write \${NAME}`);
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
headers[name.toLowerCase()] = value.trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { config: problems.length ? null : { endpoint, headers }, problems };
|
|
47
|
+
}
|
|
48
|
+
/** The vault secrets a config's headers name. */
|
|
49
|
+
export function telemetrySecretNames(cfg) {
|
|
50
|
+
return Object.values(cfg.headers).map((v) => SECRET_REF.exec(v)?.[1]).filter((n) => !!n);
|
|
51
|
+
}
|
|
52
|
+
/** Header values with each `${NAME}` filled by `lookup`; the names it could not fill. */
|
|
53
|
+
export function telemetryHeaders(cfg, lookup) {
|
|
54
|
+
const headers = {};
|
|
55
|
+
const missing = [];
|
|
56
|
+
for (const [k, v] of Object.entries(cfg.headers)) {
|
|
57
|
+
const ref = SECRET_REF.exec(v)?.[1];
|
|
58
|
+
if (!ref)
|
|
59
|
+
headers[k] = v;
|
|
60
|
+
else {
|
|
61
|
+
const value = lookup(ref);
|
|
62
|
+
if (value === null)
|
|
63
|
+
missing.push(ref);
|
|
64
|
+
else
|
|
65
|
+
headers[k] = value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { headers, missing };
|
|
69
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RunRecord } from "./store.ts";
|
|
2
|
+
export { parseTelemetry, telemetrySecretNames, telemetryHeaders, type TelemetryConfig } from "./otel-config.ts";
|
|
3
|
+
type AttrValue = {
|
|
4
|
+
stringValue: string;
|
|
5
|
+
} | {
|
|
6
|
+
intValue: string;
|
|
7
|
+
} | {
|
|
8
|
+
doubleValue: number;
|
|
9
|
+
} | {
|
|
10
|
+
boolValue: boolean;
|
|
11
|
+
};
|
|
12
|
+
type Attr = {
|
|
13
|
+
key: string;
|
|
14
|
+
value: AttrValue;
|
|
15
|
+
};
|
|
16
|
+
export interface TraceContext {
|
|
17
|
+
tenant: string;
|
|
18
|
+
workspace: string;
|
|
19
|
+
/** Agent name → its owner (owner: …), stamped on its step spans. */
|
|
20
|
+
owners?: Record<string, string | null>;
|
|
21
|
+
/** The platform version, as the instrumentation scope's. */
|
|
22
|
+
version?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The ExportTraceServiceRequest for one run. */
|
|
25
|
+
export declare function runToOtlp(run: RunRecord, ctx: TraceContext, now?: number): {
|
|
26
|
+
resourceSpans: {
|
|
27
|
+
resource: {
|
|
28
|
+
attributes: Attr[];
|
|
29
|
+
};
|
|
30
|
+
scopeSpans: {
|
|
31
|
+
scope: {
|
|
32
|
+
name: string;
|
|
33
|
+
version?: string | undefined;
|
|
34
|
+
};
|
|
35
|
+
spans: unknown[];
|
|
36
|
+
}[];
|
|
37
|
+
}[];
|
|
38
|
+
};
|