@deepstrike/sdk 0.2.30 → 0.2.32
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/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/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/kernel.d.ts +7 -1
- package/dist/providers/anthropic-compatible.d.ts +23 -0
- package/dist/providers/anthropic-compatible.js +29 -0
- 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.js +10 -22
- package/dist/providers/gemini.d.ts +12 -0
- package/dist/providers/gemini.js +38 -3
- package/dist/providers/glm.d.ts +7 -4
- package/dist/providers/glm.js +27 -24
- 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 +33 -159
- package/dist/providers/openai-responses.d.ts +6 -0
- package/dist/providers/openai-responses.js +22 -2
- package/dist/providers/openai.d.ts +52 -0
- package/dist/providers/openai.js +145 -66
- package/dist/providers/profiles.d.ts +60 -0
- package/dist/providers/profiles.js +22 -0
- package/dist/providers/qwen.d.ts +20 -19
- package/dist/providers/qwen.js +49 -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 +66 -0
- package/dist/runtime/event-stream.d.ts +44 -0
- package/dist/runtime/event-stream.js +39 -0
- 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 +56 -7
- 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/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { SignalSource } from "../signals/types.js";
|
|
|
5
5
|
import type { SessionLog, SessionEvent } from "./session-log.js";
|
|
6
6
|
import type { ArchiveStore } from "./archive.js";
|
|
7
7
|
import type { ExecutionPlane } from "./execution-plane.js";
|
|
8
|
+
import type { RunGroup } from "./run-group.js";
|
|
8
9
|
import { type MemoryPolicy, type ResourceQuota } from "../kernel.js";
|
|
9
10
|
import type { AgentRunSpec, MilestoneCheckResult, MilestoneContract, MilestonePolicy, WorkflowSpec } from "../types/agent.js";
|
|
10
11
|
import { type SubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
@@ -117,6 +118,14 @@ export interface RuntimeOptions {
|
|
|
117
118
|
* and memory-write syscalls are admitted unconditionally (pre-M2 behavior).
|
|
118
119
|
*/
|
|
119
120
|
resourceQuota?: ResourceQuota;
|
|
121
|
+
/**
|
|
122
|
+
* L1 (RunGroup): bind this runner to a governance domain shared by N peer sessions of one logical
|
|
123
|
+
* run. Members pass the same `id` + `budgetStore`; the kernel's run-level token cap is then enforced
|
|
124
|
+
* against the group's cumulative spend (seeded at boot, charged at run end) rather than per-vehicle.
|
|
125
|
+
* Unset ⇒ N=1, pre-L1 per-run budget (byte-identical). Only cumulative budget is shared; instantaneous
|
|
126
|
+
* concurrency stays vehicle-scoped (spec §2.5).
|
|
127
|
+
*/
|
|
128
|
+
runGroup?: RunGroup;
|
|
120
129
|
/**
|
|
121
130
|
* Optional long-term memory policy (`set_memory_policy`). Tunes the kernel's memory subsystem
|
|
122
131
|
* (retrieval top-k, stale-warning age, write validation, memory path). Unset leaves the kernel
|
package/dist/runtime/runner.js
CHANGED
|
@@ -146,7 +146,7 @@ export class RuntimeRunner {
|
|
|
146
146
|
* exactly as a mid-run spawn would be. Must run BEFORE `start_run` so the in-kernel gate enforces
|
|
147
147
|
* every policy from the first spawn. No config ⇒ the native-profile defaults (铁律: defaults only).
|
|
148
148
|
*/
|
|
149
|
-
applyKernelPolicies(runtime) {
|
|
149
|
+
applyKernelPolicies(runtime, groupTokensBase, groupSpawnsBase) {
|
|
150
150
|
// K2: lower governance / attention / scheduler / quota in ONE `configure_run` event instead of
|
|
151
151
|
// the previous 2–4 separate `set_*` / `load_governance_policy` events. The kernel applies each
|
|
152
152
|
// present field via the same path its granular event uses; absent fields are left untouched.
|
|
@@ -168,12 +168,19 @@ export class RuntimeRunner {
|
|
|
168
168
|
const q = this.opts.resourceQuota;
|
|
169
169
|
config.resource_quota = {
|
|
170
170
|
...(q.maxConcurrentSubagents !== undefined ? { max_concurrent_subagents: q.maxConcurrentSubagents } : {}),
|
|
171
|
+
...(q.maxTotalSubagents !== undefined ? { max_total_subagents: q.maxTotalSubagents } : {}),
|
|
171
172
|
...(q.maxSpawnDepth !== undefined ? { max_spawn_depth: q.maxSpawnDepth } : {}),
|
|
172
173
|
...(q.memoryWritesPerWindow !== undefined
|
|
173
174
|
? { memory_writes_per_window: [q.memoryWritesPerWindow.maxWrites, q.memoryWritesPerWindow.windowMs] }
|
|
174
175
|
: {}),
|
|
175
176
|
};
|
|
176
177
|
}
|
|
178
|
+
if (groupTokensBase !== undefined && groupTokensBase > 0) {
|
|
179
|
+
config.group_tokens_base = groupTokensBase;
|
|
180
|
+
}
|
|
181
|
+
if (groupSpawnsBase !== undefined && groupSpawnsBase > 0) {
|
|
182
|
+
config.group_spawns_base = groupSpawnsBase;
|
|
183
|
+
}
|
|
177
184
|
kernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
178
185
|
}
|
|
179
186
|
async appendMemorySyscallObservations(sessionId, observations) {
|
|
@@ -435,7 +442,18 @@ export class RuntimeRunner {
|
|
|
435
442
|
// already set by an in-flight `run()`) keep the original in-place behavior with no teardown.
|
|
436
443
|
const bootstrapped = !this.activeKernel || !this.currentSessionId;
|
|
437
444
|
if (bootstrapped) {
|
|
438
|
-
|
|
445
|
+
const sessionId = opts?.sessionId ?? `wf-${crypto.randomUUID()}`;
|
|
446
|
+
// L1: a standalone workflow is a member of its runner's governance domain too. Seed the
|
|
447
|
+
// bootstrap kernel with the group's cumulative spend (so the cumulative spawn/token cap bites
|
|
448
|
+
// while scheduling DAG nodes) and register membership — mirroring `execute()`. Mid-run callers
|
|
449
|
+
// skip this: their parent `run()` already seeds + counts the nodes via `localSubagentsSpawned()`.
|
|
450
|
+
let groupLedger;
|
|
451
|
+
if (this.opts.runGroup) {
|
|
452
|
+
const g = this.opts.runGroup;
|
|
453
|
+
groupLedger = await g.budgetStore.read(g.id);
|
|
454
|
+
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId });
|
|
455
|
+
}
|
|
456
|
+
this.bootstrapWorkflowKernel(sessionId, spec, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned);
|
|
439
457
|
}
|
|
440
458
|
const parentSessionId = this.currentSessionId;
|
|
441
459
|
const runtime = this.activeKernel;
|
|
@@ -453,6 +471,17 @@ export class RuntimeRunner {
|
|
|
453
471
|
}
|
|
454
472
|
finally {
|
|
455
473
|
if (bootstrapped) {
|
|
474
|
+
// L1: charge the standalone workflow's node spawns back to the group so the cumulative spawn
|
|
475
|
+
// cap (`maxTotalSubagents`) counts workflow nodes — they are member runs whose own
|
|
476
|
+
// `execute()` charge contributes 0 spawns, so without this the node count is invisible to the
|
|
477
|
+
// group. The envelope kernel's TaskTable holds one proc per scheduled node, so
|
|
478
|
+
// `localSubagentsSpawned()` is exactly that node count (the envelope itself burns no tokens).
|
|
479
|
+
if (this.opts.runGroup) {
|
|
480
|
+
const subagents = runtime.localSubagentsSpawned?.() ?? 0;
|
|
481
|
+
if (subagents > 0) {
|
|
482
|
+
await this.opts.runGroup.budgetStore.charge(this.opts.runGroup.id, { subagents });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
456
485
|
this.activeKernel = null;
|
|
457
486
|
this.currentSessionId = null;
|
|
458
487
|
this.pendingObservations = [];
|
|
@@ -466,7 +495,7 @@ export class RuntimeRunner {
|
|
|
466
495
|
* and records a `run_started` event so the standalone run is resumable from the session log. Sets
|
|
467
496
|
* `activeKernel` / `currentSessionId`; `runWorkflow` is responsible for tearing them down.
|
|
468
497
|
*/
|
|
469
|
-
bootstrapWorkflowKernel(sessionId, spec) {
|
|
498
|
+
bootstrapWorkflowKernel(sessionId, spec, groupTokensBase, groupSpawnsBase) {
|
|
470
499
|
this.interrupted = false;
|
|
471
500
|
this.abortController = new AbortController();
|
|
472
501
|
this.pendingObservations = [];
|
|
@@ -485,7 +514,7 @@ export class RuntimeRunner {
|
|
|
485
514
|
criteria: [],
|
|
486
515
|
agent_id: this.opts.agentId,
|
|
487
516
|
}).catch(() => { });
|
|
488
|
-
this.applyKernelPolicies(runtime);
|
|
517
|
+
this.applyKernelPolicies(runtime, groupTokensBase, groupSpawnsBase);
|
|
489
518
|
// K1: no explicit `start_run` — the host `load_workflow` (fired next by `runWorkflow`) self-bootstraps
|
|
490
519
|
// the run on the 0.2.30 core, matching the agent-reachable `submit_workflow` path.
|
|
491
520
|
return runtime;
|
|
@@ -542,7 +571,7 @@ export class RuntimeRunner {
|
|
|
542
571
|
if (!source)
|
|
543
572
|
return null;
|
|
544
573
|
while (!batchState.settled) {
|
|
545
|
-
const sig = await source.nextSignal();
|
|
574
|
+
const sig = await source.nextSignal(this.currentSessionId ?? undefined);
|
|
546
575
|
if (batchState.settled)
|
|
547
576
|
break;
|
|
548
577
|
if (!sig) {
|
|
@@ -867,6 +896,7 @@ export class RuntimeRunner {
|
|
|
867
896
|
maxTokens: this.opts.maxTokens,
|
|
868
897
|
maxTurns: effectiveMaxTurns,
|
|
869
898
|
timeoutMs: effectiveTimeoutMs !== undefined ? BigInt(effectiveTimeoutMs) : undefined,
|
|
899
|
+
maxTotalTokens: this.opts.maxTotalTokens !== undefined ? BigInt(this.opts.maxTotalTokens) : undefined,
|
|
870
900
|
});
|
|
871
901
|
this.activeKernel = runtime;
|
|
872
902
|
this.nextArchiveStart = nextCompressedArchiveStart;
|
|
@@ -1004,7 +1034,16 @@ export class RuntimeRunner {
|
|
|
1004
1034
|
: baseSpec;
|
|
1005
1035
|
startPayload.run_spec = agentRunSpecToKernel(spec);
|
|
1006
1036
|
}
|
|
1007
|
-
|
|
1037
|
+
// L1: seed the kernel with the group's cumulative spend so the run-level token cap + cumulative
|
|
1038
|
+
// spawn cap span the whole governance domain (other members' prior spend). No group ⇒ per-run.
|
|
1039
|
+
// Also register this session as a member so the run's lineage (R2) spans personas/invocations.
|
|
1040
|
+
let groupLedger;
|
|
1041
|
+
if (this.opts.runGroup) {
|
|
1042
|
+
const g = this.opts.runGroup;
|
|
1043
|
+
groupLedger = await g.budgetStore.read(g.id);
|
|
1044
|
+
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId });
|
|
1045
|
+
}
|
|
1046
|
+
this.applyKernelPolicies(runtime, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned);
|
|
1008
1047
|
// Multimodal upload: seed the user's attachments (images/audio) as a history
|
|
1009
1048
|
// message before start_run pushes the "[TASK STATE]" anchor. init_task does not
|
|
1010
1049
|
// clear history, so order becomes [attachment user msg, "Proceed…"] — both land
|
|
@@ -1061,7 +1100,7 @@ export class RuntimeRunner {
|
|
|
1061
1100
|
break;
|
|
1062
1101
|
}
|
|
1063
1102
|
if (this.opts.signalSource) {
|
|
1064
|
-
const sig = await this.opts.signalSource.nextSignal();
|
|
1103
|
+
const sig = await this.opts.signalSource.nextSignal(this.currentSessionId ?? undefined);
|
|
1065
1104
|
if (sig) {
|
|
1066
1105
|
// Kernel-routed: the kernel decides disposition (dedup/queue/interrupt) and emits
|
|
1067
1106
|
// `signal_disposed`. An actionable disposition yields a new action to adopt; queued/observed/
|
|
@@ -1484,6 +1523,14 @@ export class RuntimeRunner {
|
|
|
1484
1523
|
turnsUsed,
|
|
1485
1524
|
totalTokens,
|
|
1486
1525
|
}));
|
|
1526
|
+
// L1: charge this vehicle's local spend (tokens + sub-agent spawns) back to the governance domain
|
|
1527
|
+
// so the next member is seeded with the updated cumulative totals.
|
|
1528
|
+
if (this.opts.runGroup) {
|
|
1529
|
+
const subagents = runtime.localSubagentsSpawned?.() ?? 0;
|
|
1530
|
+
if (totalTokens > 0 || subagents > 0) {
|
|
1531
|
+
await this.opts.runGroup.budgetStore.charge(this.opts.runGroup.id, { tokens: totalTokens, subagents });
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1487
1534
|
if (this.opts.dreamStore && this.opts.agentId) {
|
|
1488
1535
|
const newMsgs = runtime.drainNewMessages().map(m => ({
|
|
1489
1536
|
role: m.role,
|
|
@@ -1925,6 +1972,8 @@ function signalToKernelEvent(sig) {
|
|
|
1925
1972
|
summary: String(sig.payload?.goal ?? sig.kind ?? "signal"),
|
|
1926
1973
|
payload: sig.payload ?? {},
|
|
1927
1974
|
...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
|
|
1975
|
+
...(sig.recipient ? { recipient: sig.recipient } : {}),
|
|
1976
|
+
...(sig.topic ? { topic: sig.topic } : {}),
|
|
1928
1977
|
timestamp_ms: Date.now(),
|
|
1929
1978
|
},
|
|
1930
1979
|
};
|
|
@@ -275,6 +275,14 @@ export type SessionEvent = {
|
|
|
275
275
|
kind: "summary_upgraded";
|
|
276
276
|
compressed_seq: number;
|
|
277
277
|
summary: string;
|
|
278
|
+
} | {
|
|
279
|
+
kind: "group_member_joined";
|
|
280
|
+
session_id: string;
|
|
281
|
+
role?: string;
|
|
282
|
+
} | {
|
|
283
|
+
kind: "group_budget_charged";
|
|
284
|
+
tokens: number;
|
|
285
|
+
subagents: number;
|
|
278
286
|
};
|
|
279
287
|
export interface SessionLog {
|
|
280
288
|
append(sessionId: string, event: SessionEvent): Promise<number>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 (TurnPolicy) — who reacts to a blackboard event. This is the one caller-customizable seam of a
|
|
3
|
+
* `ReactiveSession`; the framework supplies a spanning default set (addressing / delegated /
|
|
4
|
+
* deterministic-cyclic) plus combinators, so teams compose rather than hand-roll turn-taking.
|
|
5
|
+
*
|
|
6
|
+
* Stateful policies (e.g. `roundRobin`) must keep their cursor in `state`, which a `ReactiveSession`
|
|
7
|
+
* persists to the group store so it survives stateless turns.
|
|
8
|
+
*/
|
|
9
|
+
import type { BlackboardEvent } from "./event-stream.js";
|
|
10
|
+
/** A peer the policy can choose to activate. */
|
|
11
|
+
export interface PeerView {
|
|
12
|
+
personaId: string;
|
|
13
|
+
role?: string;
|
|
14
|
+
channels?: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Decide which peers react to `event`. Async to allow LLM-delegated selection. `state` is opaque,
|
|
18
|
+
* persisted across turns by the session (for cursors etc.); mutate-and-return it.
|
|
19
|
+
*/
|
|
20
|
+
export type TurnPolicy = (event: BlackboardEvent, peers: PeerView[], state: Record<string, unknown>) => string[] | Promise<string[]>;
|
|
21
|
+
/** Addressing: react iff the event names the peer (its `audience`, or its id/role in the payload). */
|
|
22
|
+
export declare function reactByMention(): TurnPolicy;
|
|
23
|
+
/**
|
|
24
|
+
* Delegated: a designated persona (or fn) decides who reacts. The most flexible escape hatch — the
|
|
25
|
+
* selector can be an LLM call. `select` receives the event + candidate peers and returns chosen ids.
|
|
26
|
+
*/
|
|
27
|
+
export declare function directorDriven(directorId: string, select: (event: BlackboardEvent, peers: PeerView[]) => string[] | Promise<string[]>): TurnPolicy;
|
|
28
|
+
/** Deterministic: cycle through peers in order, one per event. Cursor persisted in `state`. */
|
|
29
|
+
export declare function roundRobin(): TurnPolicy;
|
|
30
|
+
/** Combinator: first policy that selects a non-empty set wins (e.g. mention, else director). */
|
|
31
|
+
export declare function firstNonEmpty(...policies: TurnPolicy[]): TurnPolicy;
|
|
32
|
+
/** Combinator: union of all policies' selections (deduped, order-stable). */
|
|
33
|
+
export declare function union(...policies: TurnPolicy[]): TurnPolicy;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const idsOf = (peers) => peers.map(p => p.personaId);
|
|
2
|
+
/** Addressing: react iff the event names the peer (its `audience`, or its id/role in the payload). */
|
|
3
|
+
export function reactByMention() {
|
|
4
|
+
return (event, peers) => {
|
|
5
|
+
const hay = typeof event.payload === "string" ? event.payload : JSON.stringify(event.payload ?? "");
|
|
6
|
+
return idsOf(peers).filter(id => {
|
|
7
|
+
const peer = peers.find(p => p.personaId === id);
|
|
8
|
+
if (event.audience?.includes(id))
|
|
9
|
+
return true;
|
|
10
|
+
if (hay.includes(id))
|
|
11
|
+
return true;
|
|
12
|
+
return peer.role !== undefined && hay.includes(peer.role);
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Delegated: a designated persona (or fn) decides who reacts. The most flexible escape hatch — the
|
|
18
|
+
* selector can be an LLM call. `select` receives the event + candidate peers and returns chosen ids.
|
|
19
|
+
*/
|
|
20
|
+
export function directorDriven(directorId, select) {
|
|
21
|
+
return async (event, peers) => {
|
|
22
|
+
const chosen = await select(event, peers);
|
|
23
|
+
const valid = new Set(idsOf(peers));
|
|
24
|
+
return chosen.filter(id => valid.has(id) && id !== directorId);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Deterministic: cycle through peers in order, one per event. Cursor persisted in `state`. */
|
|
28
|
+
export function roundRobin() {
|
|
29
|
+
return (event, peers, state) => {
|
|
30
|
+
if (peers.length === 0)
|
|
31
|
+
return [];
|
|
32
|
+
const cursor = typeof state.rrCursor === "number" ? state.rrCursor : 0;
|
|
33
|
+
const idx = cursor % peers.length;
|
|
34
|
+
state.rrCursor = cursor + 1;
|
|
35
|
+
return [peers[idx].personaId];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Combinator: first policy that selects a non-empty set wins (e.g. mention, else director). */
|
|
39
|
+
export function firstNonEmpty(...policies) {
|
|
40
|
+
return async (event, peers, state) => {
|
|
41
|
+
for (const p of policies) {
|
|
42
|
+
const chosen = await p(event, peers, state);
|
|
43
|
+
if (chosen.length > 0)
|
|
44
|
+
return chosen;
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Combinator: union of all policies' selections (deduped, order-stable). */
|
|
50
|
+
export function union(...policies) {
|
|
51
|
+
return async (event, peers, state) => {
|
|
52
|
+
const out = new Set();
|
|
53
|
+
for (const p of policies)
|
|
54
|
+
for (const id of await p(event, peers, state))
|
|
55
|
+
out.add(id);
|
|
56
|
+
return [...out];
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -16,8 +16,13 @@ export declare class SignalGateway implements SignalSource {
|
|
|
16
16
|
private timers;
|
|
17
17
|
private queue;
|
|
18
18
|
private listeners;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Called by the agent loop each turn. Returns the oldest queued signal or null.
|
|
21
|
+
* When `recipient` is given, returns only the oldest signal addressed to it (plus
|
|
22
|
+
* unaddressed broadcasts); signals addressed to other recipients stay queued, so one
|
|
23
|
+
* shared gateway can serve N peer loops. Omit ⇒ legacy FIFO drain (any signal).
|
|
24
|
+
*/
|
|
25
|
+
nextSignal(recipient?: string): Promise<RuntimeSignal | null>;
|
|
21
26
|
/** Register a listener that is called synchronously whenever a signal is emitted. */
|
|
22
27
|
onSignal(listener: (sig: RuntimeSignal) => void): void;
|
|
23
28
|
/** Schedule a ScheduledPrompt to fire at its `runAtMs`. Idempotent by goal+time. */
|
package/dist/signals/gateway.js
CHANGED
|
@@ -15,9 +15,19 @@ export class SignalGateway {
|
|
|
15
15
|
queue = [];
|
|
16
16
|
listeners = [];
|
|
17
17
|
// ── SignalSource interface (pull model) ─────────────────────────────────────
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Called by the agent loop each turn. Returns the oldest queued signal or null.
|
|
20
|
+
* When `recipient` is given, returns only the oldest signal addressed to it (plus
|
|
21
|
+
* unaddressed broadcasts); signals addressed to other recipients stay queued, so one
|
|
22
|
+
* shared gateway can serve N peer loops. Omit ⇒ legacy FIFO drain (any signal).
|
|
23
|
+
*/
|
|
24
|
+
async nextSignal(recipient) {
|
|
25
|
+
if (recipient === undefined)
|
|
26
|
+
return this.queue.shift() ?? null;
|
|
27
|
+
const idx = this.queue.findIndex(s => s.recipient === undefined || s.recipient === recipient);
|
|
28
|
+
if (idx === -1)
|
|
29
|
+
return null;
|
|
30
|
+
return this.queue.splice(idx, 1)[0];
|
|
21
31
|
}
|
|
22
32
|
// ── Push API ────────────────────────────────────────────────────────────────
|
|
23
33
|
/** Register a listener that is called synchronously whenever a signal is emitted. */
|
package/dist/signals/types.d.ts
CHANGED
|
@@ -7,11 +7,20 @@ export interface RuntimeSignal {
|
|
|
7
7
|
urgency: RuntimeSignalUrgency;
|
|
8
8
|
payload: Record<string, unknown>;
|
|
9
9
|
dedupeKey?: string;
|
|
10
|
+
/** Target a specific session loop (its `sessionId`). Omitted ⇒ broadcast (any puller). */
|
|
11
|
+
recipient?: string;
|
|
12
|
+
/** Optional pub/sub topic (carried through; multi-subscriber routing deferred). */
|
|
13
|
+
topic?: string;
|
|
10
14
|
/** @deprecated Use source/signalType/urgency directly. */
|
|
11
15
|
kind?: "interrupt" | "scheduled" | "external";
|
|
12
16
|
/** @deprecated Prefer explicit `urgency`. */
|
|
13
17
|
priority?: number;
|
|
14
18
|
}
|
|
15
19
|
export interface SignalSource {
|
|
16
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Pull the next pending signal. When `recipient` is given, return only signals
|
|
22
|
+
* addressed to it (plus unaddressed broadcasts); other recipients' signals stay
|
|
23
|
+
* queued. Omit ⇒ legacy FIFO drain (any signal).
|
|
24
|
+
*/
|
|
25
|
+
nextSignal(recipient?: string): Promise<RuntimeSignal | null>;
|
|
17
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.32",
|
|
4
4
|
"description": "DeepStrike Node.js SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@anthropic-ai/sdk": "^0.99.0",
|
|
75
|
-
"@deepstrike/core": "0.2.
|
|
75
|
+
"@deepstrike/core": "0.2.32",
|
|
76
76
|
"@google/generative-ai": "^0.24.1",
|
|
77
77
|
"openai": "^5.23.2"
|
|
78
78
|
},
|