@deepstrike/sdk 0.2.28 → 0.2.31
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 +45 -21
- package/dist/harness/harness.d.ts +3 -2
- package/dist/harness/harness.js +15 -35
- package/dist/harness/judge.d.ts +42 -0
- package/dist/harness/judge.js +58 -0
- package/dist/harness/public.d.ts +4 -0
- package/dist/harness/public.js +3 -0
- package/dist/index.d.ts +21 -80
- package/dist/index.js +28 -60
- package/dist/kernel.d.ts +7 -1
- package/dist/memory/public.d.ts +4 -0
- package/dist/memory/public.js +3 -0
- package/dist/os/public.d.ts +18 -0
- package/dist/os/public.js +14 -0
- package/dist/planes/public.d.ts +13 -0
- package/dist/planes/public.js +9 -0
- package/dist/providers/anthropic-compatible.d.ts +23 -0
- package/dist/providers/anthropic-compatible.js +29 -0
- package/dist/providers/anthropic.d.ts +11 -2
- package/dist/providers/anthropic.js +14 -9
- package/dist/providers/catalog.js +5 -53
- package/dist/providers/deepseek.d.ts +28 -8
- package/dist/providers/deepseek.js +38 -157
- package/dist/providers/factories.d.ts +31 -0
- package/dist/providers/factories.js +33 -0
- package/dist/providers/glm.d.ts +5 -4
- package/dist/providers/glm.js +8 -23
- package/dist/providers/kimi.d.ts +5 -4
- package/dist/providers/kimi.js +8 -22
- package/dist/providers/minimax.d.ts +26 -12
- package/dist/providers/minimax.js +32 -158
- package/dist/providers/openai.d.ts +57 -2
- package/dist/providers/openai.js +139 -70
- package/dist/providers/public.d.ts +10 -0
- package/dist/providers/public.js +11 -0
- package/dist/providers/qwen.d.ts +19 -19
- package/dist/providers/qwen.js +37 -176
- package/dist/providers/registry.d.ts +18 -0
- package/dist/providers/registry.js +35 -0
- package/dist/providers/vendor-profiles.d.ts +54 -0
- package/dist/providers/vendor-profiles.js +62 -0
- package/dist/runtime/event-stream.d.ts +44 -0
- package/dist/runtime/event-stream.js +39 -0
- package/dist/runtime/facade.js +6 -1
- package/dist/runtime/reactive-session.d.ts +125 -0
- package/dist/runtime/reactive-session.js +127 -0
- package/dist/runtime/run-group.d.ts +74 -0
- package/dist/runtime/run-group.js +72 -0
- package/dist/runtime/runner.d.ts +9 -0
- package/dist/runtime/runner.js +79 -46
- package/dist/runtime/session-log.d.ts +8 -0
- package/dist/runtime/turn-policy.d.ts +33 -0
- package/dist/runtime/turn-policy.js +58 -0
- package/dist/signals/gateway.d.ts +7 -2
- package/dist/signals/gateway.js +13 -3
- package/dist/signals/types.d.ts +10 -1
- package/dist/workflow/public.d.ts +20 -0
- package/dist/workflow/public.js +15 -0
- package/package.json +54 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AnthropicProvider } from "./anthropic.js";
|
|
2
|
+
import { OpenAIChatProvider } from "./openai.js";
|
|
3
|
+
import { OpenAIResponsesProvider } from "./openai-responses.js";
|
|
4
|
+
import { DeepSeekProvider, DeepSeekAnthropicProvider } from "./deepseek.js";
|
|
5
|
+
import { KimiProvider, KimiAnthropicProvider } from "./kimi.js";
|
|
6
|
+
import { QwenProvider, QwenAnthropicProvider } from "./qwen.js";
|
|
7
|
+
import { GLMProvider, GLMAnthropicProvider } from "./glm.js";
|
|
8
|
+
import { MiniMaxOpenAIProvider, MiniMaxAnthropicProvider } from "./minimax.js";
|
|
9
|
+
import { GeminiProvider } from "./gemini.js";
|
|
10
|
+
/** Build the registry key for a `(providerId, endpointProtocol)` pair. */
|
|
11
|
+
export function providerRegistryKey(providerId, protocol) {
|
|
12
|
+
return `${providerId}:${protocol}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Single source of truth for which provider class backs each `(vendor, wire)` pair. Consumed by
|
|
16
|
+
* both `createProvider` (catalog) and the per-backend factory functions, so the two can no longer
|
|
17
|
+
* drift. Adding a vendor/wire = add a row here (+ its `vendor-profiles` / `endpointProfiles` data) —
|
|
18
|
+
* no dispatch branch to edit. Values are the same named classes as before, so `instanceof` holds.
|
|
19
|
+
*/
|
|
20
|
+
export const PROVIDER_REGISTRY = {
|
|
21
|
+
"anthropic:anthropic-messages": (k, m, r, b) => new AnthropicProvider(k, m, r, { baseURL: b }),
|
|
22
|
+
"openai:openai-chat": (k, m, r, b) => new OpenAIChatProvider(k, m, r, b),
|
|
23
|
+
"openai:openai-responses": (k, m, r, b) => new OpenAIResponsesProvider(k, m, r, b),
|
|
24
|
+
"deepseek:openai-chat": (k, m, r, b) => new DeepSeekProvider(k, m, r, b),
|
|
25
|
+
"deepseek:anthropic-messages": (k, m, r, b) => new DeepSeekAnthropicProvider(k, m, r, b),
|
|
26
|
+
"kimi:openai-chat": (k, m, r, b) => new KimiProvider(k, m, r, b),
|
|
27
|
+
"kimi:anthropic-messages": (k, m, r, b) => new KimiAnthropicProvider(k, m, r, b),
|
|
28
|
+
"qwen:openai-chat": (k, m, r, b) => new QwenProvider(k, m, r, b),
|
|
29
|
+
"qwen:anthropic-messages": (k, m, r, b) => new QwenAnthropicProvider(k, m, r, b),
|
|
30
|
+
"glm:openai-chat": (k, m, r, b) => new GLMProvider(k, m, r, b),
|
|
31
|
+
"glm:anthropic-messages": (k, m, r, b) => new GLMAnthropicProvider(k, m, r, b),
|
|
32
|
+
"minimax:openai-chat": (k, m, r, b) => new MiniMaxOpenAIProvider(k, m, r, b),
|
|
33
|
+
"minimax:anthropic-messages": (k, m, r, b) => new MiniMaxAnthropicProvider(k, m, r, b),
|
|
34
|
+
"gemini:gemini": (k, m, r, b) => new GeminiProvider(k, m, r, b),
|
|
35
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RuntimePolicy } from "../types.js";
|
|
2
|
+
import { endpointProfiles } from "./profiles.js";
|
|
3
|
+
import type { ProviderId } from "./profiles.js";
|
|
4
|
+
export type EndpointProfileKey = keyof typeof endpointProfiles;
|
|
5
|
+
export interface AnthropicVendorProfile {
|
|
6
|
+
/** Identity advertised in `descriptor().provider`. */
|
|
7
|
+
providerId: ProviderId;
|
|
8
|
+
/** Model used when the caller does not pass one. */
|
|
9
|
+
defaultModel: string;
|
|
10
|
+
/** Endpoint profile whose `baseURL` is the Anthropic-compatible wire for this vendor. */
|
|
11
|
+
baseURLProfileKey: EndpointProfileKey;
|
|
12
|
+
/** Recommended `maxTurns` per model id; missing model → empty policy. */
|
|
13
|
+
policies: Record<string, RuntimePolicy>;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEEPSEEK_POLICIES: Record<string, RuntimePolicy>;
|
|
16
|
+
export declare const KIMI_POLICIES: Record<string, RuntimePolicy>;
|
|
17
|
+
export declare const QWEN_POLICIES: Record<string, RuntimePolicy>;
|
|
18
|
+
export declare const GLM_POLICIES: Record<string, RuntimePolicy>;
|
|
19
|
+
export declare const MINIMAX_POLICIES: Record<string, RuntimePolicy>;
|
|
20
|
+
export declare const anthropicVendorProfiles: {
|
|
21
|
+
deepseek: {
|
|
22
|
+
providerId: "deepseek";
|
|
23
|
+
defaultModel: string;
|
|
24
|
+
baseURLProfileKey: "deepseek.anthropic";
|
|
25
|
+
policies: Record<string, RuntimePolicy>;
|
|
26
|
+
};
|
|
27
|
+
kimi: {
|
|
28
|
+
providerId: "kimi";
|
|
29
|
+
defaultModel: string;
|
|
30
|
+
baseURLProfileKey: "kimi.anthropic";
|
|
31
|
+
policies: Record<string, RuntimePolicy>;
|
|
32
|
+
};
|
|
33
|
+
qwen: {
|
|
34
|
+
providerId: "qwen";
|
|
35
|
+
defaultModel: string;
|
|
36
|
+
baseURLProfileKey: "qwen.anthropic";
|
|
37
|
+
policies: Record<string, RuntimePolicy>;
|
|
38
|
+
};
|
|
39
|
+
glm: {
|
|
40
|
+
providerId: "glm";
|
|
41
|
+
defaultModel: string;
|
|
42
|
+
baseURLProfileKey: "glm.anthropic";
|
|
43
|
+
policies: Record<string, RuntimePolicy>;
|
|
44
|
+
};
|
|
45
|
+
minimax: {
|
|
46
|
+
providerId: "minimax";
|
|
47
|
+
defaultModel: string;
|
|
48
|
+
baseURLProfileKey: "minimax.anthropic";
|
|
49
|
+
policies: Record<string, RuntimePolicy>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export type AnthropicVendorId = keyof typeof anthropicVendorProfiles;
|
|
53
|
+
/** Resolve the Anthropic-compatible base URL for a vendor profile. */
|
|
54
|
+
export declare function anthropicVendorBaseURL(profile: AnthropicVendorProfile): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { endpointProfiles } from "./profiles.js";
|
|
2
|
+
export const DEEPSEEK_POLICIES = {
|
|
3
|
+
"deepseek-chat": { maxTurns: 25 },
|
|
4
|
+
"deepseek-reasoner": { maxTurns: 50 },
|
|
5
|
+
"deepseek-v4-flash": { maxTurns: 20 },
|
|
6
|
+
"deepseek-v4-pro": { maxTurns: 35 },
|
|
7
|
+
};
|
|
8
|
+
export const KIMI_POLICIES = {
|
|
9
|
+
"moonshot-v1-8k": { maxTurns: 15 },
|
|
10
|
+
"moonshot-v1-32k": { maxTurns: 20 },
|
|
11
|
+
"moonshot-v1-128k": { maxTurns: 30 },
|
|
12
|
+
"kimi-k2.5": { maxTurns: 30 },
|
|
13
|
+
"kimi-k2.6": { maxTurns: 35 },
|
|
14
|
+
"kimi-k2-thinking": { maxTurns: 50 },
|
|
15
|
+
"kimi-k2-thinking-turbo": { maxTurns: 40 },
|
|
16
|
+
};
|
|
17
|
+
export const QWEN_POLICIES = {
|
|
18
|
+
"qwen3.7-max-preview": { maxTurns: 45 },
|
|
19
|
+
"qwen3.7-plus-preview": { maxTurns: 40 },
|
|
20
|
+
"qwen3.6-max-preview": { maxTurns: 40 },
|
|
21
|
+
"qwen3.6-plus": { maxTurns: 35 },
|
|
22
|
+
"qwen3.6-flash": { maxTurns: 20 },
|
|
23
|
+
"qwen3.6-35b-a3b": { maxTurns: 25 },
|
|
24
|
+
"qwen3.6-27b": { maxTurns: 25 },
|
|
25
|
+
"qwen3.5-plus": { maxTurns: 35 },
|
|
26
|
+
"qwen3.5-flash": { maxTurns: 20 },
|
|
27
|
+
"qwen3.5-397b-a17b": { maxTurns: 35 },
|
|
28
|
+
"qwen3.5-122b-a10b": { maxTurns: 25 },
|
|
29
|
+
"qwen3.5-35b-a3b": { maxTurns: 20 },
|
|
30
|
+
"qwen3.5-27b": { maxTurns: 20 },
|
|
31
|
+
};
|
|
32
|
+
export const GLM_POLICIES = {
|
|
33
|
+
"glm-5.1": { maxTurns: 50 },
|
|
34
|
+
"glm/glm-5.1": { maxTurns: 50 },
|
|
35
|
+
"glm-4-plus": { maxTurns: 35 },
|
|
36
|
+
"glm/glm-4-plus": { maxTurns: 35 },
|
|
37
|
+
"glm-4-flash": { maxTurns: 15 },
|
|
38
|
+
"glm/glm-4-flash": { maxTurns: 15 },
|
|
39
|
+
"glm-4-air": { maxTurns: 20 },
|
|
40
|
+
"glm/glm-4-air": { maxTurns: 20 },
|
|
41
|
+
};
|
|
42
|
+
export const MINIMAX_POLICIES = {
|
|
43
|
+
"MiniMax-M2.7": { maxTurns: 35 },
|
|
44
|
+
"MiniMax-M2.7-highspeed": { maxTurns: 35 },
|
|
45
|
+
"MiniMax-M2.5": { maxTurns: 25 },
|
|
46
|
+
"MiniMax-M2.5-highspeed": { maxTurns: 25 },
|
|
47
|
+
"MiniMax-M2.1": { maxTurns: 25 },
|
|
48
|
+
"MiniMax-M2.1-highspeed": { maxTurns: 25 },
|
|
49
|
+
"MiniMax-M2": { maxTurns: 20 },
|
|
50
|
+
"MiniMax-Text-01": { maxTurns: 20 },
|
|
51
|
+
};
|
|
52
|
+
export const anthropicVendorProfiles = {
|
|
53
|
+
deepseek: { providerId: "deepseek", defaultModel: "deepseek-v4-flash", baseURLProfileKey: "deepseek.anthropic", policies: DEEPSEEK_POLICIES },
|
|
54
|
+
kimi: { providerId: "kimi", defaultModel: "kimi-k2.6", baseURLProfileKey: "kimi.anthropic", policies: KIMI_POLICIES },
|
|
55
|
+
qwen: { providerId: "qwen", defaultModel: "qwen3.6-plus", baseURLProfileKey: "qwen.anthropic", policies: QWEN_POLICIES },
|
|
56
|
+
glm: { providerId: "glm", defaultModel: "glm-5.1", baseURLProfileKey: "glm.anthropic", policies: GLM_POLICIES },
|
|
57
|
+
minimax: { providerId: "minimax", defaultModel: "MiniMax-M2.7", baseURLProfileKey: "minimax.anthropic", policies: MINIMAX_POLICIES },
|
|
58
|
+
};
|
|
59
|
+
/** Resolve the Anthropic-compatible base URL for a vendor profile. */
|
|
60
|
+
export function anthropicVendorBaseURL(profile) {
|
|
61
|
+
return endpointProfiles[profile.baseURLProfileKey].baseURL;
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 (Blackboard) — a shared, append-only event stream that N peer agent sessions of one logical run
|
|
3
|
+
* observe. This is the pluggable storage seam (like `SessionLog`): the default `InMemoryEventStream`
|
|
4
|
+
* is process-local; back it with Postgres/Redis to span replicas/restarts.
|
|
5
|
+
*
|
|
6
|
+
* Visibility (spec §6.1): events are shared by default. Optional `channel` / `audience` tags scope an
|
|
7
|
+
* event to a subset of personas, enforced at the framework boundary (`readSince(seq, viewer)` + the
|
|
8
|
+
* `read_recent` tool) — context isolation, not convention.
|
|
9
|
+
*/
|
|
10
|
+
/** One entry on the shared blackboard. `channel`/`audience` are optional visibility scoping. */
|
|
11
|
+
export interface BlackboardEvent {
|
|
12
|
+
seq: number;
|
|
13
|
+
payload: unknown;
|
|
14
|
+
/** Emitting persona id (or external source), for audit / `reactByMention`. */
|
|
15
|
+
source?: string;
|
|
16
|
+
/** Channel this event belongs to; only personas subscribed to it see it. Omit ⇒ all see it. */
|
|
17
|
+
channel?: string;
|
|
18
|
+
/** Explicit recipient persona ids; only they see it. Omit ⇒ all see it (subject to `channel`). */
|
|
19
|
+
audience?: string[];
|
|
20
|
+
}
|
|
21
|
+
/** A reader's identity for visibility filtering. */
|
|
22
|
+
export interface EventViewer {
|
|
23
|
+
personaId: string;
|
|
24
|
+
/** Channels this persona is subscribed to. */
|
|
25
|
+
channels?: string[];
|
|
26
|
+
}
|
|
27
|
+
/** Default full-share visibility rule (spec §6.1). */
|
|
28
|
+
export declare function isVisibleTo(event: Pick<BlackboardEvent, "channel" | "audience">, viewer: EventViewer): boolean;
|
|
29
|
+
export interface EventStream {
|
|
30
|
+
/** Append an event; returns it stamped with its assigned `seq`. */
|
|
31
|
+
append(event: Omit<BlackboardEvent, "seq">): Promise<BlackboardEvent>;
|
|
32
|
+
/** Events after `seq`. With a `viewer`, only those visible to it (default: all). */
|
|
33
|
+
readSince(seq: number, viewer?: EventViewer): Promise<BlackboardEvent[]>;
|
|
34
|
+
/** Notify a listener on each appended event. Returns an unsubscribe fn. */
|
|
35
|
+
subscribe(cb: (e: BlackboardEvent) => void): () => void;
|
|
36
|
+
}
|
|
37
|
+
/** Process-local default blackboard. */
|
|
38
|
+
export declare class InMemoryEventStream implements EventStream {
|
|
39
|
+
private readonly events;
|
|
40
|
+
private readonly listeners;
|
|
41
|
+
append(event: Omit<BlackboardEvent, "seq">): Promise<BlackboardEvent>;
|
|
42
|
+
readSince(seq: number, viewer?: EventViewer): Promise<BlackboardEvent[]>;
|
|
43
|
+
subscribe(cb: (e: BlackboardEvent) => void): () => void;
|
|
44
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 (Blackboard) — a shared, append-only event stream that N peer agent sessions of one logical run
|
|
3
|
+
* observe. This is the pluggable storage seam (like `SessionLog`): the default `InMemoryEventStream`
|
|
4
|
+
* is process-local; back it with Postgres/Redis to span replicas/restarts.
|
|
5
|
+
*
|
|
6
|
+
* Visibility (spec §6.1): events are shared by default. Optional `channel` / `audience` tags scope an
|
|
7
|
+
* event to a subset of personas, enforced at the framework boundary (`readSince(seq, viewer)` + the
|
|
8
|
+
* `read_recent` tool) — context isolation, not convention.
|
|
9
|
+
*/
|
|
10
|
+
/** Default full-share visibility rule (spec §6.1). */
|
|
11
|
+
export function isVisibleTo(event, viewer) {
|
|
12
|
+
if (event.audience === undefined && event.channel === undefined)
|
|
13
|
+
return true;
|
|
14
|
+
if (event.audience?.includes(viewer.personaId))
|
|
15
|
+
return true;
|
|
16
|
+
if (event.channel !== undefined && viewer.channels?.includes(event.channel))
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
/** Process-local default blackboard. */
|
|
21
|
+
export class InMemoryEventStream {
|
|
22
|
+
events = [];
|
|
23
|
+
listeners = new Set();
|
|
24
|
+
async append(event) {
|
|
25
|
+
const stamped = { ...event, seq: this.events.length };
|
|
26
|
+
this.events.push(stamped);
|
|
27
|
+
for (const l of this.listeners)
|
|
28
|
+
l(stamped);
|
|
29
|
+
return stamped;
|
|
30
|
+
}
|
|
31
|
+
async readSince(seq, viewer) {
|
|
32
|
+
const after = this.events.filter(e => e.seq > seq);
|
|
33
|
+
return viewer ? after.filter(e => isVisibleTo(e, viewer)) : after;
|
|
34
|
+
}
|
|
35
|
+
subscribe(cb) {
|
|
36
|
+
this.listeners.add(cb);
|
|
37
|
+
return () => this.listeners.delete(cb);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/runtime/facade.js
CHANGED
|
@@ -58,6 +58,11 @@ export async function runFanout(opts) {
|
|
|
58
58
|
],
|
|
59
59
|
};
|
|
60
60
|
const outcome = await runner.runWorkflow(spec, opts.sessionId ? { sessionId: opts.sessionId } : undefined);
|
|
61
|
+
// The synthesis node is the last spec node; the kernel ids nodes `wf-node{index}`. Prefer that id,
|
|
62
|
+
// but fall back to the last completed node's output so a kernel id-scheme change can't silently
|
|
63
|
+
// return an empty synthesis.
|
|
61
64
|
const synthesisId = `wf-node${opts.tasks.length}`;
|
|
62
|
-
|
|
65
|
+
const lastCompleted = outcome.completed[outcome.completed.length - 1];
|
|
66
|
+
const synthesis = outcome.outputs[synthesisId] ?? (lastCompleted ? outcome.outputs[lastCompleted] : undefined) ?? "";
|
|
67
|
+
return { synthesis, outputs: outcome.outputs };
|
|
63
68
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 (ReactiveSession) — the user-facing primitive for "N peer agents over a shared event stream"
|
|
3
|
+
* (spec §6). It composes the lower layers so teams don't hand-roll the pattern:
|
|
4
|
+
* - L1 `RunGroup` — shared governance domain (cumulative budget + lineage) across the personas.
|
|
5
|
+
* - L0 `SignalGateway`— recipient-routed signals (targeted `interrupt` / `broadcast`).
|
|
6
|
+
* - `EventStream` — the shared blackboard (pluggable; default in-memory).
|
|
7
|
+
* - `TurnPolicy` — who reacts to each event (the one caller-customizable seam).
|
|
8
|
+
*
|
|
9
|
+
* Stateless-friendly: `emit` can run inside an HTTP handler; each persona's turn is a normal
|
|
10
|
+
* `run({sessionId})` whose continuity comes from its `SessionLog`, and `resume()` rebuilds the peer
|
|
11
|
+
* set from the persisted `RunGroup` membership — no hot in-process loop required.
|
|
12
|
+
*/
|
|
13
|
+
import type { RuntimeRunner } from "./runner.js";
|
|
14
|
+
import type { RunGroup } from "./run-group.js";
|
|
15
|
+
import type { SignalSource, RuntimeSignal } from "../signals/types.js";
|
|
16
|
+
import { SignalGateway } from "../os/public.js";
|
|
17
|
+
import type { BlackboardEvent, EventStream, EventViewer } from "./event-stream.js";
|
|
18
|
+
import type { TurnPolicy } from "./turn-policy.js";
|
|
19
|
+
import type { RegisteredTool } from "../tools/index.js";
|
|
20
|
+
/**
|
|
21
|
+
* How a persona executes one reactive turn. The default body is a single `runner.run(...)` agent turn;
|
|
22
|
+
* override it to make a persona's turn a *different orchestration form* — e.g. drive a DAG via
|
|
23
|
+
* `ctx.runner.runWorkflow(spec)` (DAG-in-Peer) or any composite. The runner is already wired to the
|
|
24
|
+
* shared `RunGroup`, so whatever the body spawns stays under one governance domain. Must return the
|
|
25
|
+
* persona's reaction text.
|
|
26
|
+
*/
|
|
27
|
+
export interface ReactorContext {
|
|
28
|
+
personaId: string;
|
|
29
|
+
goal: string;
|
|
30
|
+
event: BlackboardEvent;
|
|
31
|
+
/** The persona's runner — wired to the shared RunGroup / signal gateway / blackboard. */
|
|
32
|
+
runner: RuntimeRunner;
|
|
33
|
+
}
|
|
34
|
+
export type ReactorTurn = (ctx: ReactorContext) => Promise<string>;
|
|
35
|
+
/** Per-persona registration: its base reaction goal, role, channel subscriptions, and turn body. */
|
|
36
|
+
export interface ReactivePeerSpec {
|
|
37
|
+
goal?: string;
|
|
38
|
+
role?: string;
|
|
39
|
+
channels?: string[];
|
|
40
|
+
/**
|
|
41
|
+
* Override this persona's turn body (the seam for composing other mechanisms into a peer). Defaults
|
|
42
|
+
* to the session `reactWith`, then to a single `run()` agent turn. Use to make this peer's reaction
|
|
43
|
+
* a workflow DAG, a nested ensemble, etc. — all under the shared `RunGroup`.
|
|
44
|
+
*/
|
|
45
|
+
react?: ReactorTurn;
|
|
46
|
+
}
|
|
47
|
+
/** What the caller appends to the blackboard via `emit`. */
|
|
48
|
+
export interface EmitEvent {
|
|
49
|
+
payload: unknown;
|
|
50
|
+
source?: string;
|
|
51
|
+
channel?: string;
|
|
52
|
+
audience?: string[];
|
|
53
|
+
}
|
|
54
|
+
export interface ReactiveSessionOptions {
|
|
55
|
+
/** Shared governance domain — all personas run under it (L1). */
|
|
56
|
+
runGroup: RunGroup;
|
|
57
|
+
/** Who reacts to each event (L2). */
|
|
58
|
+
turnPolicy: TurnPolicy;
|
|
59
|
+
/** Shared blackboard. Defaults to a process-local `InMemoryEventStream`. */
|
|
60
|
+
eventStream?: EventStream;
|
|
61
|
+
/** Shared signal gateway for targeted interrupt / broadcast (L0). Defaults to a fresh one. */
|
|
62
|
+
signalGateway?: SignalGateway;
|
|
63
|
+
/**
|
|
64
|
+
* Build a runner for a persona, wiring in the shared governance + signal routing. The app owns the
|
|
65
|
+
* provider / execution plane / tools; spread `shared` into the `RuntimeRunner` options and register
|
|
66
|
+
* `readRecentTool(shared.eventStream, viewer)` so the persona can read the blackboard.
|
|
67
|
+
*/
|
|
68
|
+
makeRunner: (personaId: string, shared: {
|
|
69
|
+
runGroup: RunGroup;
|
|
70
|
+
signalSource: SignalSource;
|
|
71
|
+
eventStream: EventStream;
|
|
72
|
+
}) => RuntimeRunner;
|
|
73
|
+
/** Goal for a persona's reactive turn. Defaults to a generic "react to the blackboard" prompt. */
|
|
74
|
+
goalFor?: (personaId: string, event: BlackboardEvent) => string;
|
|
75
|
+
/**
|
|
76
|
+
* Default turn body for peers that don't set their own `react`. Defaults to a single `run()` agent
|
|
77
|
+
* turn. Override to make every peer's turn a different orchestration form (e.g. a workflow DAG).
|
|
78
|
+
*/
|
|
79
|
+
reactWith?: ReactorTurn;
|
|
80
|
+
}
|
|
81
|
+
/** A persona's reaction to an emitted event. */
|
|
82
|
+
export interface Reaction {
|
|
83
|
+
personaId: string;
|
|
84
|
+
output: string;
|
|
85
|
+
}
|
|
86
|
+
export declare class ReactiveSession {
|
|
87
|
+
private readonly opts;
|
|
88
|
+
private readonly peerSpecs;
|
|
89
|
+
private readonly runners;
|
|
90
|
+
private readonly policyState;
|
|
91
|
+
private readonly eventStream;
|
|
92
|
+
private readonly gateway;
|
|
93
|
+
constructor(opts: ReactiveSessionOptions);
|
|
94
|
+
/** Register a peer persona and record it in the group membership (lineage). */
|
|
95
|
+
addPeer(personaId: string, spec?: ReactivePeerSpec): void;
|
|
96
|
+
peers(): string[];
|
|
97
|
+
blackboard(): EventStream;
|
|
98
|
+
/**
|
|
99
|
+
* Append an event to the blackboard, ask the `TurnPolicy` which (visible) peers react, and drive one
|
|
100
|
+
* turn for each — returning their outputs. Each turn runs under the shared `RunGroup` governance.
|
|
101
|
+
*/
|
|
102
|
+
emit(event: EmitEvent): Promise<Reaction[]>;
|
|
103
|
+
/** Targeted preemption: deliver a critical signal to one persona's loop only (L0 recipient routing). */
|
|
104
|
+
interrupt(personaId: string, signal: Partial<RuntimeSignal> & {
|
|
105
|
+
payload?: Record<string, unknown>;
|
|
106
|
+
}): Promise<void>;
|
|
107
|
+
/** Broadcast a signal to every persona (each sees it on its next turn). */
|
|
108
|
+
broadcast(signal: Partial<RuntimeSignal> & {
|
|
109
|
+
payload?: Record<string, unknown>;
|
|
110
|
+
}): Promise<void>;
|
|
111
|
+
private getRunner;
|
|
112
|
+
private driveTurn;
|
|
113
|
+
/**
|
|
114
|
+
* Rebuild a session from a persisted `RunGroup`: load its members (lineage) as peers. The blackboard
|
|
115
|
+
* continuity comes from the (persistent) `EventStream`. Turn-policy cursor state is not restored.
|
|
116
|
+
*/
|
|
117
|
+
static resume(opts: ReactiveSessionOptions & {
|
|
118
|
+
peerSpecs?: Record<string, ReactivePeerSpec>;
|
|
119
|
+
}): Promise<ReactiveSession>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A `read_recent` tool a persona uses to read the shared blackboard, scoped to what it may see. Register
|
|
123
|
+
* one per persona inside `makeRunner`. `viewer` is the reading persona (id + subscribed channels).
|
|
124
|
+
*/
|
|
125
|
+
export declare function readRecentTool(eventStream: EventStream, viewer: EventViewer): RegisteredTool;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { collectText } from "./runner.js";
|
|
2
|
+
import { SignalGateway } from "../os/public.js";
|
|
3
|
+
import { InMemoryEventStream, isVisibleTo } from "./event-stream.js";
|
|
4
|
+
import { tool } from "../tools/index.js";
|
|
5
|
+
export class ReactiveSession {
|
|
6
|
+
opts;
|
|
7
|
+
peerSpecs = new Map();
|
|
8
|
+
runners = new Map();
|
|
9
|
+
policyState = {};
|
|
10
|
+
eventStream;
|
|
11
|
+
gateway;
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
this.opts = opts;
|
|
14
|
+
this.eventStream = opts.eventStream ?? new InMemoryEventStream();
|
|
15
|
+
this.gateway = opts.signalGateway ?? new SignalGateway();
|
|
16
|
+
}
|
|
17
|
+
/** Register a peer persona and record it in the group membership (lineage). */
|
|
18
|
+
addPeer(personaId, spec = {}) {
|
|
19
|
+
this.peerSpecs.set(personaId, spec);
|
|
20
|
+
void this.opts.runGroup.budgetStore.join(this.opts.runGroup.id, { sessionId: personaId, role: spec.role });
|
|
21
|
+
}
|
|
22
|
+
peers() {
|
|
23
|
+
return [...this.peerSpecs.keys()];
|
|
24
|
+
}
|
|
25
|
+
blackboard() {
|
|
26
|
+
return this.eventStream;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Append an event to the blackboard, ask the `TurnPolicy` which (visible) peers react, and drive one
|
|
30
|
+
* turn for each — returning their outputs. Each turn runs under the shared `RunGroup` governance.
|
|
31
|
+
*/
|
|
32
|
+
async emit(event) {
|
|
33
|
+
const bbEvent = await this.eventStream.append(event);
|
|
34
|
+
const candidates = [...this.peerSpecs.entries()]
|
|
35
|
+
.map(([personaId, spec]) => ({ personaId, role: spec.role, channels: spec.channels }))
|
|
36
|
+
// Only personas that can actually see the event are eligible to react.
|
|
37
|
+
.filter(p => isVisibleTo(bbEvent, p));
|
|
38
|
+
const chosen = await this.opts.turnPolicy(bbEvent, candidates, this.policyState);
|
|
39
|
+
const eligible = new Set(candidates.map(p => p.personaId));
|
|
40
|
+
const reactions = [];
|
|
41
|
+
for (const personaId of chosen) {
|
|
42
|
+
if (!eligible.has(personaId))
|
|
43
|
+
continue;
|
|
44
|
+
reactions.push({ personaId, output: await this.driveTurn(personaId, bbEvent) });
|
|
45
|
+
}
|
|
46
|
+
return reactions;
|
|
47
|
+
}
|
|
48
|
+
/** Targeted preemption: deliver a critical signal to one persona's loop only (L0 recipient routing). */
|
|
49
|
+
async interrupt(personaId, signal) {
|
|
50
|
+
this.gateway.ingest({
|
|
51
|
+
source: "gateway",
|
|
52
|
+
signalType: "alert",
|
|
53
|
+
urgency: "critical",
|
|
54
|
+
payload: signal.payload ?? {},
|
|
55
|
+
...signal,
|
|
56
|
+
recipient: personaId,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Broadcast a signal to every persona (each sees it on its next turn). */
|
|
60
|
+
async broadcast(signal) {
|
|
61
|
+
this.gateway.ingest({
|
|
62
|
+
source: "gateway",
|
|
63
|
+
signalType: "event",
|
|
64
|
+
urgency: "normal",
|
|
65
|
+
payload: signal.payload ?? {},
|
|
66
|
+
...signal,
|
|
67
|
+
recipient: undefined,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
getRunner(personaId) {
|
|
71
|
+
let runner = this.runners.get(personaId);
|
|
72
|
+
if (!runner) {
|
|
73
|
+
runner = this.opts.makeRunner(personaId, {
|
|
74
|
+
runGroup: this.opts.runGroup,
|
|
75
|
+
signalSource: this.gateway,
|
|
76
|
+
eventStream: this.eventStream,
|
|
77
|
+
});
|
|
78
|
+
this.runners.set(personaId, runner);
|
|
79
|
+
}
|
|
80
|
+
return runner;
|
|
81
|
+
}
|
|
82
|
+
async driveTurn(personaId, event) {
|
|
83
|
+
const runner = this.getRunner(personaId);
|
|
84
|
+
const goal = this.opts.goalFor?.(personaId, event) ??
|
|
85
|
+
this.peerSpecs.get(personaId)?.goal ??
|
|
86
|
+
"React to the latest events on the shared blackboard.";
|
|
87
|
+
// Turn-body seam (the DAG-in-Peer enabler): a persona's reaction can be any orchestration form,
|
|
88
|
+
// not just a single agent turn. Per-peer `react` wins, then session `reactWith`, else the default
|
|
89
|
+
// `run()`. Whatever the body drives (e.g. `runner.runWorkflow`) inherits the shared RunGroup.
|
|
90
|
+
const react = this.peerSpecs.get(personaId)?.react ?? this.opts.reactWith;
|
|
91
|
+
if (react)
|
|
92
|
+
return react({ personaId, goal, event, runner });
|
|
93
|
+
// run() with the persona's stable sessionId replays its prior turns from the SessionLog, so this
|
|
94
|
+
// is continuity-preserving whether or not the persona has acted before (stateless-handler safe).
|
|
95
|
+
return collectText(runner.run({ sessionId: personaId, goal }));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Rebuild a session from a persisted `RunGroup`: load its members (lineage) as peers. The blackboard
|
|
99
|
+
* continuity comes from the (persistent) `EventStream`. Turn-policy cursor state is not restored.
|
|
100
|
+
*/
|
|
101
|
+
static async resume(opts) {
|
|
102
|
+
const session = new ReactiveSession(opts);
|
|
103
|
+
for (const member of await opts.runGroup.budgetStore.members(opts.runGroup.id)) {
|
|
104
|
+
session.peerSpecs.set(member.sessionId, opts.peerSpecs?.[member.sessionId] ?? { role: member.role });
|
|
105
|
+
}
|
|
106
|
+
return session;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A `read_recent` tool a persona uses to read the shared blackboard, scoped to what it may see. Register
|
|
111
|
+
* one per persona inside `makeRunner`. `viewer` is the reading persona (id + subscribed channels).
|
|
112
|
+
*/
|
|
113
|
+
export function readRecentTool(eventStream, viewer) {
|
|
114
|
+
return tool("read_recent", "Read recent events from the shared blackboard visible to you (optionally a single channel).", {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
since_seq: { type: "number", description: "Only events after this seq (default: from the start)." },
|
|
118
|
+
channel: { type: "string", description: "Restrict to one channel you subscribe to." },
|
|
119
|
+
},
|
|
120
|
+
}, async (args) => {
|
|
121
|
+
const sinceSeq = typeof args.since_seq === "number" ? args.since_seq : -1;
|
|
122
|
+
const channel = typeof args.channel === "string" ? args.channel : undefined;
|
|
123
|
+
const events = await eventStream.readSince(sinceSeq, viewer);
|
|
124
|
+
const filtered = channel ? events.filter(e => e.channel === channel) : events;
|
|
125
|
+
return JSON.stringify(filtered.map(e => ({ seq: e.seq, source: e.source, channel: e.channel, payload: e.payload })));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L1 (RunGroup) — a governance domain shared by N peer agent sessions of one logical run.
|
|
3
|
+
*
|
|
4
|
+
* The kernel (execution vehicle) is ephemeral and torn down between stateless turns, so the
|
|
5
|
+
* cumulative budget + membership that must span the whole group live outside any vehicle: in a
|
|
6
|
+
* `GroupBudgetStore`. Each member's run is seeded at boot with the group's accumulated spend (tokens
|
|
7
|
+
* + sub-agent spawns) so the run-level token cap and the cumulative spawn cap are enforced across all
|
|
8
|
+
* members, registers itself as a member (lineage), and charges its own consumption back when it ends.
|
|
9
|
+
* Per spec §2.5, only *cumulative* budget is shared this way; instantaneous concurrency stays
|
|
10
|
+
* vehicle-scoped.
|
|
11
|
+
*
|
|
12
|
+
* Two built-in stores:
|
|
13
|
+
* - `InMemoryGroupBudgetStore` — process-local; fine for a single replica / tests.
|
|
14
|
+
* - `SessionLogGroupBudgetStore` — persists the ledger + membership to any `SessionLog` (fold-on-read
|
|
15
|
+
* under a group-anchor key), so a logical run's governance + lineage survive process boundaries and
|
|
16
|
+
* span replicas when backed by a durable `SessionLog`.
|
|
17
|
+
*/
|
|
18
|
+
import type { SessionLog } from "./session-log.js";
|
|
19
|
+
/** Cumulative resources spent across a run group. */
|
|
20
|
+
export interface GroupLedger {
|
|
21
|
+
/** Total tokens spent by all members. */
|
|
22
|
+
tokensSpent: number;
|
|
23
|
+
/** Total sub-agents spawned by all members (running + completed). */
|
|
24
|
+
subagentsSpawned: number;
|
|
25
|
+
}
|
|
26
|
+
/** A member's contribution to charge back to the group ledger. */
|
|
27
|
+
export interface GroupCharge {
|
|
28
|
+
tokens?: number;
|
|
29
|
+
subagents?: number;
|
|
30
|
+
}
|
|
31
|
+
/** A persona session that participated in the logical run (process-table lineage). */
|
|
32
|
+
export interface GroupMember {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
role?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface GroupBudgetStore {
|
|
37
|
+
/** Cumulative spend across the group so far. */
|
|
38
|
+
read(groupId: string): GroupLedger | Promise<GroupLedger>;
|
|
39
|
+
/** Add a member's spend to the group's cumulative totals. */
|
|
40
|
+
charge(groupId: string, delta: GroupCharge): void | Promise<void>;
|
|
41
|
+
/** Register a persona session as a member of the group (idempotent by sessionId). */
|
|
42
|
+
join(groupId: string, member: GroupMember): void | Promise<void>;
|
|
43
|
+
/** All persona sessions of the logical run — the cross-invocation lineage (R2). */
|
|
44
|
+
members(groupId: string): GroupMember[] | Promise<GroupMember[]>;
|
|
45
|
+
}
|
|
46
|
+
/** Process-local default store. One ledger + member set per group id. */
|
|
47
|
+
export declare class InMemoryGroupBudgetStore implements GroupBudgetStore {
|
|
48
|
+
private readonly ledgers;
|
|
49
|
+
private readonly memberships;
|
|
50
|
+
read(groupId: string): GroupLedger;
|
|
51
|
+
charge(groupId: string, delta: GroupCharge): void;
|
|
52
|
+
join(groupId: string, member: GroupMember): void;
|
|
53
|
+
members(groupId: string): GroupMember[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Persists the group ledger + membership to a `SessionLog`, keyed by a group-anchor session whose id
|
|
57
|
+
* is the group id. Budget/membership rebuild by folding `group_budget_charged` / `group_member_joined`
|
|
58
|
+
* events on read (spec §2.4). Durable + replica-spanning when the underlying `SessionLog` is.
|
|
59
|
+
*/
|
|
60
|
+
export declare class SessionLogGroupBudgetStore implements GroupBudgetStore {
|
|
61
|
+
private readonly log;
|
|
62
|
+
constructor(log: SessionLog);
|
|
63
|
+
read(groupId: string): Promise<GroupLedger>;
|
|
64
|
+
charge(groupId: string, delta: GroupCharge): Promise<void>;
|
|
65
|
+
join(groupId: string, member: GroupMember): Promise<void>;
|
|
66
|
+
members(groupId: string): Promise<GroupMember[]>;
|
|
67
|
+
}
|
|
68
|
+
/** Binds a runner to a governance domain: a stable group id + the store its members share. */
|
|
69
|
+
export interface RunGroup {
|
|
70
|
+
/** Stable id for this logical run's governance domain; all members pass the same one. */
|
|
71
|
+
id: string;
|
|
72
|
+
/** Shared cumulative-budget + membership store. */
|
|
73
|
+
budgetStore: GroupBudgetStore;
|
|
74
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Process-local default store. One ledger + member set per group id. */
|
|
2
|
+
export class InMemoryGroupBudgetStore {
|
|
3
|
+
ledgers = new Map();
|
|
4
|
+
memberships = new Map();
|
|
5
|
+
read(groupId) {
|
|
6
|
+
return this.ledgers.get(groupId) ?? { tokensSpent: 0, subagentsSpawned: 0 };
|
|
7
|
+
}
|
|
8
|
+
charge(groupId, delta) {
|
|
9
|
+
const cur = this.read(groupId);
|
|
10
|
+
this.ledgers.set(groupId, {
|
|
11
|
+
tokensSpent: cur.tokensSpent + Math.max(0, delta.tokens ?? 0),
|
|
12
|
+
subagentsSpawned: cur.subagentsSpawned + Math.max(0, delta.subagents ?? 0),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
join(groupId, member) {
|
|
16
|
+
if (!this.memberships.has(groupId))
|
|
17
|
+
this.memberships.set(groupId, new Map());
|
|
18
|
+
this.memberships.get(groupId).set(member.sessionId, member);
|
|
19
|
+
}
|
|
20
|
+
members(groupId) {
|
|
21
|
+
return [...(this.memberships.get(groupId)?.values() ?? [])];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Persists the group ledger + membership to a `SessionLog`, keyed by a group-anchor session whose id
|
|
26
|
+
* is the group id. Budget/membership rebuild by folding `group_budget_charged` / `group_member_joined`
|
|
27
|
+
* events on read (spec §2.4). Durable + replica-spanning when the underlying `SessionLog` is.
|
|
28
|
+
*/
|
|
29
|
+
export class SessionLogGroupBudgetStore {
|
|
30
|
+
log;
|
|
31
|
+
constructor(log) {
|
|
32
|
+
this.log = log;
|
|
33
|
+
}
|
|
34
|
+
async read(groupId) {
|
|
35
|
+
let tokensSpent = 0;
|
|
36
|
+
let subagentsSpawned = 0;
|
|
37
|
+
for (const { event } of await this.log.read(groupId)) {
|
|
38
|
+
if (event.kind === "group_budget_charged") {
|
|
39
|
+
tokensSpent += event.tokens;
|
|
40
|
+
subagentsSpawned += event.subagents;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { tokensSpent, subagentsSpawned };
|
|
44
|
+
}
|
|
45
|
+
async charge(groupId, delta) {
|
|
46
|
+
await this.log.append(groupId, {
|
|
47
|
+
kind: "group_budget_charged",
|
|
48
|
+
tokens: Math.max(0, delta.tokens ?? 0),
|
|
49
|
+
subagents: Math.max(0, delta.subagents ?? 0),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async join(groupId, member) {
|
|
53
|
+
// Idempotent: don't grow the log with duplicate joins for the same session.
|
|
54
|
+
const existing = await this.members(groupId);
|
|
55
|
+
if (existing.some(m => m.sessionId === member.sessionId))
|
|
56
|
+
return;
|
|
57
|
+
await this.log.append(groupId, {
|
|
58
|
+
kind: "group_member_joined",
|
|
59
|
+
session_id: member.sessionId,
|
|
60
|
+
...(member.role ? { role: member.role } : {}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async members(groupId) {
|
|
64
|
+
const seen = new Map();
|
|
65
|
+
for (const { event } of await this.log.read(groupId)) {
|
|
66
|
+
if (event.kind === "group_member_joined") {
|
|
67
|
+
seen.set(event.session_id, { sessionId: event.session_id, role: event.role });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...seen.values()];
|
|
71
|
+
}
|
|
72
|
+
}
|