specrails-core 6.4.0 → 6.6.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.
Files changed (131) hide show
  1. package/dist/agent-runtime/capabilities.d.ts +2 -2
  2. package/dist/agent-runtime/capabilities.js +22 -5
  3. package/dist/agent-runtime/cli-executor.js +8 -3
  4. package/dist/agent-runtime/cli.d.ts +4 -4
  5. package/dist/agent-runtime/cli.js +50 -3
  6. package/dist/agent-runtime/compact/environment.d.ts +50 -4
  7. package/dist/agent-runtime/compact/environment.js +183 -6
  8. package/dist/agent-runtime/compact/step.d.ts +1 -1
  9. package/dist/agent-runtime/efficiency-summary.d.ts +1 -1
  10. package/dist/agent-runtime/efficiency-types.d.ts +1 -1
  11. package/dist/agent-runtime/engine/checkpoint/database.d.ts +15 -15
  12. package/dist/agent-runtime/engine/contracts.d.ts +2 -1
  13. package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
  14. package/dist/agent-runtime/engine/efficiency-summary.d.ts +8 -8
  15. package/dist/agent-runtime/engine/fork.d.ts +1 -1
  16. package/dist/agent-runtime/engine/graph-description.d.ts +16 -16
  17. package/dist/agent-runtime/engine/implementation-compiler.d.ts +68 -68
  18. package/dist/agent-runtime/engine/piece-dependencies.js +4 -4
  19. package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +1 -1
  20. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
  21. package/dist/agent-runtime/engine/pieces/role-turn.js +64 -22
  22. package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
  23. package/dist/agent-runtime/engine/pieces/verify.js +46 -13
  24. package/dist/agent-runtime/engine/run-status.d.ts +165 -165
  25. package/dist/agent-runtime/engine/runs.d.ts +168 -168
  26. package/dist/agent-runtime/engine/state.d.ts +16 -16
  27. package/dist/agent-runtime/evaluation.d.ts +3 -3
  28. package/dist/agent-runtime/executor-types.d.ts +10 -10
  29. package/dist/agent-runtime/graph/convergence.d.ts +2 -2
  30. package/dist/agent-runtime/graph/convergence.js +11 -0
  31. package/dist/agent-runtime/graph/nodes.js +35 -4
  32. package/dist/agent-runtime/graph/review-policy.d.ts +1 -1
  33. package/dist/agent-runtime/graph/state.d.ts +6 -2
  34. package/dist/agent-runtime/guardrails.d.ts +1 -1
  35. package/dist/agent-runtime/guardrails.js +4 -1
  36. package/dist/agent-runtime/index.d.ts +3 -2
  37. package/dist/agent-runtime/index.js +1 -0
  38. package/dist/agent-runtime/openai-executor.d.ts +9 -9
  39. package/dist/agent-runtime/openspec.d.ts +4 -4
  40. package/dist/agent-runtime/prompts.d.ts +6 -0
  41. package/dist/agent-runtime/prompts.js +32 -8
  42. package/dist/agent-runtime/recovery.d.ts +12 -12
  43. package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
  44. package/dist/agent-runtime/session/application/active-session.js +701 -0
  45. package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
  46. package/dist/agent-runtime/session/application/delegation.js +171 -0
  47. package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
  48. package/dist/agent-runtime/session/application/output-buffer.js +65 -0
  49. package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
  50. package/dist/agent-runtime/session/application/session-service.js +256 -0
  51. package/dist/agent-runtime/session/application/timers.d.ts +15 -0
  52. package/dist/agent-runtime/session/application/timers.js +35 -0
  53. package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
  54. package/dist/agent-runtime/session/domain/descriptor.js +32 -0
  55. package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
  56. package/dist/agent-runtime/session/domain/errors.js +16 -0
  57. package/dist/agent-runtime/session/domain/events.d.ts +147 -0
  58. package/dist/agent-runtime/session/domain/events.js +24 -0
  59. package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
  60. package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
  61. package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
  62. package/dist/agent-runtime/session/domain/interruption.js +39 -0
  63. package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
  64. package/dist/agent-runtime/session/domain/policy.js +104 -0
  65. package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
  66. package/dist/agent-runtime/session/domain/snapshot.js +234 -0
  67. package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
  68. package/dist/agent-runtime/session/domain/state-machines.js +41 -0
  69. package/dist/agent-runtime/session/domain/types.d.ts +130 -0
  70. package/dist/agent-runtime/session/domain/types.js +15 -0
  71. package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
  72. package/dist/agent-runtime/session/domain/usage.js +86 -0
  73. package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
  74. package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
  75. package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
  76. package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
  77. package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
  78. package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
  79. package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
  80. package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
  81. package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
  82. package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
  83. package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
  84. package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
  85. package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
  86. package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
  87. package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
  88. package/dist/agent-runtime/session/drivers/common/process.js +116 -0
  89. package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
  90. package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
  91. package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
  92. package/dist/agent-runtime/session/drivers/registry.js +38 -0
  93. package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
  94. package/dist/agent-runtime/session/host/protocol.js +88 -0
  95. package/dist/agent-runtime/session/host/server.d.ts +57 -0
  96. package/dist/agent-runtime/session/host/server.js +229 -0
  97. package/dist/agent-runtime/session/index.d.ts +45 -0
  98. package/dist/agent-runtime/session/index.js +74 -0
  99. package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
  100. package/dist/agent-runtime/session/journal/schema.js +44 -0
  101. package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
  102. package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
  103. package/dist/agent-runtime/session/ports.d.ts +192 -0
  104. package/dist/agent-runtime/session/ports.js +1 -0
  105. package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
  106. package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
  107. package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
  108. package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
  109. package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
  110. package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
  111. package/dist/agent-runtime/session/testing/index.d.ts +9 -0
  112. package/dist/agent-runtime/session/testing/index.js +9 -0
  113. package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
  114. package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
  115. package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
  116. package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
  117. package/dist/agent-runtime/verification-proposals.d.ts +9 -0
  118. package/dist/agent-runtime/verification-proposals.js +69 -0
  119. package/dist/agent-runtime/verification-repair.d.ts +31 -2
  120. package/dist/agent-runtime/verification-repair.js +58 -3
  121. package/dist/installer/commands/init.d.ts +1 -1
  122. package/dist/installer/phases/install-config.js +10 -3
  123. package/dist/installer/util/paths.d.ts +25 -1
  124. package/dist/installer/util/registry.js +3 -3
  125. package/dist/pipeline/pipeline-state.d.ts +42 -11
  126. package/dist/pipeline/pipeline-state.js +47 -8
  127. package/dist/shared/specrails-home.d.ts +21 -0
  128. package/dist/shared/specrails-home.js +36 -0
  129. package/integration-contract.json +42 -1
  130. package/package.json +23 -11
  131. package/schemas/agent-runtime.schema.json +1 -1
@@ -0,0 +1,73 @@
1
+ import type { SessionEventEnvelope } from '../domain/events.js';
2
+ import { type SessionPolicyInput } from '../domain/policy.js';
3
+ import type { SessionSnapshot } from '../domain/snapshot.js';
4
+ import type { Ids, SessionJournal } from '../ports.js';
5
+ import type { ActiveSession } from './active-session.js';
6
+ /** What the coordinator needs from the session service (no cycle with its internals). */
7
+ export interface DelegationPort {
8
+ journal: SessionJournal;
9
+ ids: Ids;
10
+ /** The open parent session, activated if needed; throws when unknown or closed. */
11
+ parent(sessionId: string): ActiveSession;
12
+ open(params: {
13
+ sessionId: string;
14
+ driver: string;
15
+ model: string;
16
+ effort?: string;
17
+ cwd: string;
18
+ systemPrompt: string;
19
+ policy: SessionPolicyInput;
20
+ metadata: Record<string, unknown>;
21
+ }): Promise<unknown>;
22
+ send(sessionId: string, input: {
23
+ inputId: string;
24
+ text: string;
25
+ delivery: 'queue';
26
+ }): Promise<unknown>;
27
+ interrupt(sessionId: string): Promise<unknown>;
28
+ close(sessionId: string, reason: string): Promise<void>;
29
+ snapshot(sessionId: string): SessionSnapshot;
30
+ }
31
+ export interface DelegateParams {
32
+ description: string;
33
+ prompt: string;
34
+ agentType?: string;
35
+ /** Recent parent turns (user inputs and replies) to hand over as context. */
36
+ contextTurns?: number;
37
+ }
38
+ export interface DelegatedResult {
39
+ subagentId: string;
40
+ description: string;
41
+ status: 'completed' | 'failed' | 'stopped' | 'interrupted';
42
+ result: string;
43
+ }
44
+ /**
45
+ * Core-launched sub-agents (delegated runtime). Each one is a child session on
46
+ * the policy's driver, model and effort; the parent sees it as a sub-agent of
47
+ * its own tree. Results reach the parent through an explicit wait or, when
48
+ * nobody waits, a continuation turn (see ActiveSession.delegatedFinished).
49
+ */
50
+ export declare class DelegationCoordinator {
51
+ private readonly port;
52
+ private readonly children;
53
+ constructor(port: DelegationPort);
54
+ delegate(parentId: string, params: DelegateParams): Promise<{
55
+ subagentId: string;
56
+ }>;
57
+ /** Resolve with finished results, or with what still runs when the timeout elapses. */
58
+ wait(parentId: string, subagentIds: readonly string[] | undefined, timeoutMs: number): Promise<{
59
+ results: DelegatedResult[];
60
+ running: string[];
61
+ }>;
62
+ /** Delegated ids among `ids` (all of the parent's when omitted), stopped. */
63
+ stop(parentId: string, ids?: readonly string[], reason?: string): Promise<string[]>;
64
+ /** The parent session is closing: its delegated work ends with it. */
65
+ closeParent(parentId: string): Promise<void>;
66
+ isDelegated(subagentId: string): boolean;
67
+ /** Mirror committed child events into the parent's tree. */
68
+ observe(envelopes: readonly SessionEventEnvelope[]): void;
69
+ private finish;
70
+ private parentIfOpen;
71
+ /** The task, optionally preceded by the parent's recent conversation. */
72
+ private composePrompt;
73
+ }
@@ -0,0 +1,171 @@
1
+ import { SessionError } from '../domain/errors.js';
2
+ import { delegatedSubagents } from '../domain/policy.js';
3
+ const SYSTEM_PROMPT = [
4
+ 'You are a sub-agent: another agent working in this same workspace delegated a task to you.',
5
+ 'Complete it on your own; nobody can answer questions.',
6
+ 'Finish with a concise, self-contained result the delegating agent can act on: what you found or changed, where, and anything left open.',
7
+ ].join(' ');
8
+ const MAX_CONTEXT_TURNS = 10;
9
+ /**
10
+ * Core-launched sub-agents (delegated runtime). Each one is a child session on
11
+ * the policy's driver, model and effort; the parent sees it as a sub-agent of
12
+ * its own tree. Results reach the parent through an explicit wait or, when
13
+ * nobody waits, a continuation turn (see ActiveSession.delegatedFinished).
14
+ */
15
+ export class DelegationCoordinator {
16
+ port;
17
+ children = new Map();
18
+ constructor(port) {
19
+ this.port = port;
20
+ }
21
+ async delegate(parentId, params) {
22
+ const parent = this.port.parent(parentId);
23
+ const policy = parent.policy;
24
+ if (!delegatedSubagents(policy)) {
25
+ throw new SessionError('invalid_params', 'This session does not delegate sub-agents; its provider launches them itself', { path: 'sessionId' });
26
+ }
27
+ const runtime = policy.subagentRuntime;
28
+ if (parent.delegatedLive().length >= runtime.maxConcurrent) {
29
+ throw new SessionError('limit_reached', `At most ${runtime.maxConcurrent} delegated sub-agents can run at once`, { limit: runtime.maxConcurrent });
30
+ }
31
+ const description = params.description.trim().slice(0, 200) || 'Sub-agent';
32
+ const childId = this.port.ids.session();
33
+ const cwd = this.port.journal.getSession(parentId)?.cwd;
34
+ if (!cwd)
35
+ throw new SessionError('internal', `Session ${parentId} has no working directory`);
36
+ const child = { childId, parentId, description, outcome: null, collected: false, waiters: new Set() };
37
+ this.children.set(childId, child);
38
+ parent.delegatedStarted({ subagentId: childId, description, agentType: params.agentType ?? `${runtime.driver}:${runtime.model}`, prompt: params.prompt, driver: runtime.driver, model: runtime.model });
39
+ try {
40
+ await this.port.open({
41
+ sessionId: childId,
42
+ driver: runtime.driver,
43
+ model: runtime.model,
44
+ ...(runtime.effort ? { effort: runtime.effort } : {}),
45
+ cwd,
46
+ systemPrompt: SYSTEM_PROMPT,
47
+ // Depth 1: a delegated sub-agent works alone, with the parent's permissions and MCP servers.
48
+ policy: { subagents: 'disabled', permissions: policy.permissions, mcp: { servers: policy.mcp.servers, inheritUserScope: policy.mcp.inheritUserScope } },
49
+ metadata: { parentSessionId: parentId, delegated: true, description },
50
+ });
51
+ await this.port.send(childId, { inputId: this.port.ids.input(), text: this.composePrompt(parentId, params), delivery: 'queue' });
52
+ }
53
+ catch (error) {
54
+ this.finish(child, { status: 'failed', text: '', usage: null, reason: error.message.slice(0, 300) });
55
+ throw error;
56
+ }
57
+ return { subagentId: childId };
58
+ }
59
+ /** Resolve with finished results, or with what still runs when the timeout elapses. */
60
+ async wait(parentId, subagentIds, timeoutMs) {
61
+ const targets = [...this.children.values()].filter((child) => child.parentId === parentId && (!subagentIds || subagentIds.includes(child.childId)));
62
+ const pending = targets.filter((child) => !child.outcome);
63
+ if (pending.length > 0) {
64
+ await new Promise((resolve) => {
65
+ const timer = setTimeout(done, Math.max(0, timeoutMs));
66
+ timer.unref?.();
67
+ function done() { clearTimeout(timer); for (const child of pending)
68
+ child.waiters.delete(check); resolve(); }
69
+ function check() { if (pending.every((child) => child.outcome))
70
+ done(); }
71
+ for (const child of pending)
72
+ child.waiters.add(check);
73
+ });
74
+ }
75
+ const finished = targets.filter((child) => child.outcome);
76
+ for (const child of finished)
77
+ child.collected = true;
78
+ this.parentIfOpen(parentId)?.collectDelegated(finished.map((child) => child.childId));
79
+ return {
80
+ results: finished.map((child) => ({ subagentId: child.childId, description: child.description, status: child.outcome.status, result: child.outcome.text || `It ended without a result (${child.outcome.reason ?? child.outcome.status}).` })),
81
+ running: targets.filter((child) => !child.outcome).map((child) => child.childId),
82
+ };
83
+ }
84
+ /** Delegated ids among `ids` (all of the parent's when omitted), stopped. */
85
+ async stop(parentId, ids, reason = 'host_request') {
86
+ const targets = [...this.children.values()].filter((child) => child.parentId === parentId && !child.outcome && (!ids || ids.includes(child.childId)));
87
+ await Promise.all(targets.map(async (child) => {
88
+ this.finish(child, { status: 'stopped', text: '', usage: null, reason });
89
+ await this.port.interrupt(child.childId).catch(() => undefined);
90
+ await this.port.close(child.childId, 'delegation_stopped').catch(() => undefined);
91
+ }));
92
+ return targets.map((child) => child.childId);
93
+ }
94
+ /** The parent session is closing: its delegated work ends with it. */
95
+ async closeParent(parentId) {
96
+ await this.stop(parentId, undefined, 'host_request');
97
+ for (const [childId, child] of this.children)
98
+ if (child.parentId === parentId)
99
+ this.children.delete(childId);
100
+ }
101
+ isDelegated(subagentId) {
102
+ return this.children.has(subagentId);
103
+ }
104
+ /** Mirror committed child events into the parent's tree. */
105
+ observe(envelopes) {
106
+ for (const envelope of envelopes) {
107
+ const child = this.children.get(envelope.sessionId);
108
+ if (!child || child.outcome)
109
+ continue;
110
+ const event = envelope.event;
111
+ const parent = this.parentIfOpen(child.parentId);
112
+ switch (event.type) {
113
+ case 'turn.output':
114
+ if (event.channel === 'text')
115
+ parent?.delegatedOutput(child.childId, { channel: 'text', delta: event.delta });
116
+ break;
117
+ case 'turn.tool': {
118
+ const { type: _type, turnId: _turnId, at: _at, ...tool } = event;
119
+ parent?.delegatedOutput(child.childId, { channel: 'tool', tool });
120
+ break;
121
+ }
122
+ case 'turn.completed':
123
+ this.finish(child, { status: event.status, text: event.text, usage: event.usage, ...(event.error ? { reason: event.error.slice(0, 300) } : {}) });
124
+ // Its work is done: release the child's provider process and session.
125
+ void this.port.close(child.childId, 'delegation_finished').catch(() => undefined);
126
+ break;
127
+ case 'session.closed':
128
+ this.finish(child, { status: 'interrupted', text: '', usage: null, reason: event.reason });
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ finish(child, outcome) {
134
+ if (child.outcome)
135
+ return;
136
+ child.outcome = outcome;
137
+ this.parentIfOpen(child.parentId)?.delegatedFinished(child.childId, { ...outcome, collected: child.collected });
138
+ for (const waiter of [...child.waiters])
139
+ waiter();
140
+ }
141
+ parentIfOpen(parentId) {
142
+ try {
143
+ return this.port.parent(parentId);
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /** The task, optionally preceded by the parent's recent conversation. */
150
+ composePrompt(parentId, params) {
151
+ const turns = Math.min(Math.max(params.contextTurns ?? 0, 0), MAX_CONTEXT_TURNS);
152
+ if (turns === 0)
153
+ return params.prompt;
154
+ const lines = [];
155
+ let after = Math.max(0, this.port.snapshot(parentId).lastSeq - 5_000);
156
+ for (;;) {
157
+ const page = this.port.journal.read(parentId, after, 1_000);
158
+ for (const { event } of page.events) {
159
+ if (event.type === 'input.accepted')
160
+ lines.push(`User: ${event.text}`);
161
+ if (event.type === 'turn.completed' && event.text)
162
+ lines.push(`Agent: ${event.text}`);
163
+ }
164
+ if (!page.hasMore || page.events.length === 0)
165
+ break;
166
+ after = page.events.at(-1).seq;
167
+ }
168
+ const context = lines.slice(-turns * 2).map((line) => line.slice(0, 4_000)).join('\n\n');
169
+ return context ? `Context from the delegating agent's conversation:\n\n${context}\n\n---\n\nYour task:\n${params.prompt}` : params.prompt;
170
+ }
171
+ }
@@ -0,0 +1,43 @@
1
+ import type { SessionEventBody } from '../domain/events.js';
2
+ import type { OutputChannel } from '../domain/types.js';
3
+ export interface OutputLimits {
4
+ /** Flush buffered deltas once they reach this many characters. */
5
+ flushChars: number;
6
+ /** Stored characters per turn before output is elided. */
7
+ turnCap: number;
8
+ /** Stored characters per sub-agent before output is elided. */
9
+ subagentCap: number;
10
+ }
11
+ export declare const DEFAULT_OUTPUT_LIMITS: OutputLimits;
12
+ /**
13
+ * Coalesces streaming text deltas and enforces per-turn / per-sub-agent caps.
14
+ * Deltas for the same target are merged until a size threshold, a timer, or any
15
+ * non-delta event forces a flush (which preserves ordering). Beyond the cap,
16
+ * deltas are counted but not stored; `drain*` reports one `output.truncated`.
17
+ */
18
+ export declare class OutputBuffer {
19
+ private readonly limits;
20
+ private readonly pending;
21
+ private readonly budgets;
22
+ constructor(limits?: OutputLimits);
23
+ /** Buffer a delta. Returns events that must be committed now (size-based flush). */
24
+ add(body: {
25
+ type: 'turn.output';
26
+ turnId: string;
27
+ channel: OutputChannel;
28
+ delta: string;
29
+ } | {
30
+ type: 'subagent.output';
31
+ subagentId: string;
32
+ channel: 'text';
33
+ delta: string;
34
+ }): SessionEventBody[];
35
+ hasPending(): boolean;
36
+ /** All buffered deltas, in first-buffered order. */
37
+ flush(): SessionEventBody[];
38
+ /** Truncation report for a finished turn (and forget its budget). */
39
+ drainTurn(turnId: string): SessionEventBody[];
40
+ /** Truncation report for a sub-agent that stopped running (and reset its budget). */
41
+ drainSubagent(subagentId: string): SessionEventBody[];
42
+ private drain;
43
+ }
@@ -0,0 +1,65 @@
1
+ export const DEFAULT_OUTPUT_LIMITS = Object.freeze({ flushChars: 4_096, turnCap: 4 * 1024 * 1024, subagentCap: 1024 * 1024 });
2
+ /**
3
+ * Coalesces streaming text deltas and enforces per-turn / per-sub-agent caps.
4
+ * Deltas for the same target are merged until a size threshold, a timer, or any
5
+ * non-delta event forces a flush (which preserves ordering). Beyond the cap,
6
+ * deltas are counted but not stored; `drain*` reports one `output.truncated`.
7
+ */
8
+ export class OutputBuffer {
9
+ limits;
10
+ pending = new Map();
11
+ budgets = new Map();
12
+ constructor(limits = DEFAULT_OUTPUT_LIMITS) {
13
+ this.limits = limits;
14
+ }
15
+ /** Buffer a delta. Returns events that must be committed now (size-based flush). */
16
+ add(body) {
17
+ const scope = body.type === 'turn.output' ? { turnId: body.turnId } : { subagentId: body.subagentId };
18
+ const budgetKey = body.type === 'turn.output' ? `turn:${body.turnId}` : `subagent:${body.subagentId}`;
19
+ const cap = body.type === 'turn.output' ? this.limits.turnCap : this.limits.subagentCap;
20
+ const budget = this.budgets.get(budgetKey) ?? { stored: 0, droppedEvents: 0, droppedBytes: 0, scope };
21
+ this.budgets.set(budgetKey, budget);
22
+ if (budget.stored + body.delta.length > cap) {
23
+ budget.droppedEvents += 1;
24
+ budget.droppedBytes += body.delta.length;
25
+ return [];
26
+ }
27
+ budget.stored += body.delta.length;
28
+ const key = `${budgetKey}:${body.channel}`;
29
+ const existing = this.pending.get(key);
30
+ if (existing)
31
+ existing.body.delta += body.delta;
32
+ else
33
+ this.pending.set(key, { body: { ...body } });
34
+ const current = this.pending.get(key);
35
+ if (current.body.delta.length >= this.limits.flushChars) {
36
+ this.pending.delete(key);
37
+ return [current.body];
38
+ }
39
+ return [];
40
+ }
41
+ hasPending() {
42
+ return this.pending.size > 0;
43
+ }
44
+ /** All buffered deltas, in first-buffered order. */
45
+ flush() {
46
+ const bodies = [...this.pending.values()].map((entry) => entry.body);
47
+ this.pending.clear();
48
+ return bodies;
49
+ }
50
+ /** Truncation report for a finished turn (and forget its budget). */
51
+ drainTurn(turnId) {
52
+ return this.drain(`turn:${turnId}`);
53
+ }
54
+ /** Truncation report for a sub-agent that stopped running (and reset its budget). */
55
+ drainSubagent(subagentId) {
56
+ return this.drain(`subagent:${subagentId}`);
57
+ }
58
+ drain(key) {
59
+ const budget = this.budgets.get(key);
60
+ this.budgets.delete(key);
61
+ if (!budget || budget.droppedEvents === 0)
62
+ return [];
63
+ return [{ type: 'output.truncated', scope: budget.scope, droppedEvents: budget.droppedEvents, droppedBytes: budget.droppedBytes }];
64
+ }
65
+ }
@@ -0,0 +1,121 @@
1
+ import type { SessionEventEnvelope } from '../domain/events.js';
2
+ import { type SessionPolicyInput } from '../domain/policy.js';
3
+ import { type SessionSnapshot } from '../domain/snapshot.js';
4
+ import type { Attachment, DriverDescriptor } from '../domain/types.js';
5
+ import type { RateCard } from '../domain/usage.js';
6
+ import type { Clock, DriverCatalog, Ids, SessionJournal, SessionSummary } from '../ports.js';
7
+ import { type DelegateParams, type DelegatedResult } from './delegation.js';
8
+ import type { OutputLimits } from './output-buffer.js';
9
+ export interface SessionServiceOptions {
10
+ journal: SessionJournal;
11
+ drivers: DriverCatalog;
12
+ clock: Clock;
13
+ ids: Ids;
14
+ /** Host-supplied rate cards for estimating cost when a provider reports none. */
15
+ rateCard?: (driver: string, model: string) => RateCard | null;
16
+ /** Resident processes kept alive while idle; busy sessions are never evicted. */
17
+ maxResident?: number;
18
+ outputLimits?: OutputLimits;
19
+ interruptGraceMs?: number;
20
+ }
21
+ export interface OpenParams {
22
+ sessionId?: string;
23
+ /** Reattach to an open session of this scope (e.g. after a host restart). */
24
+ resume?: {
25
+ sessionId: string;
26
+ };
27
+ driver: string;
28
+ model: string;
29
+ effort?: string;
30
+ cwd: string;
31
+ systemPrompt?: string;
32
+ policy: SessionPolicyInput;
33
+ /** Continue an existing provider conversation (e.g. a session created before Core sessions existed). */
34
+ providerSessionRef?: string;
35
+ metadata?: Record<string, unknown>;
36
+ }
37
+ export interface SendParams {
38
+ inputId: string;
39
+ text: string;
40
+ attachments?: Attachment[];
41
+ delivery: 'queue' | 'steer';
42
+ }
43
+ export type SessionListener = (envelopes: SessionEventEnvelope[]) => void;
44
+ export declare const DEFAULT_MAX_RESIDENT = 6;
45
+ /**
46
+ * Application facade for one scope: the use cases behind every protocol method.
47
+ * Operations on one session are serialized; different sessions run concurrently.
48
+ */
49
+ export declare class SessionService {
50
+ private readonly options;
51
+ private readonly active;
52
+ private readonly locks;
53
+ private readonly listeners;
54
+ private shuttingDown;
55
+ private readonly delegation;
56
+ constructor(options: SessionServiceOptions);
57
+ drivers(): DriverDescriptor[];
58
+ subscribe(listener: SessionListener): () => void;
59
+ /**
60
+ * Mark work a previous host left running as interrupted. Called once at
61
+ * startup before serving requests; never starts a provider process.
62
+ */
63
+ recover(reason?: 'restart' | 'host_lost'): string[];
64
+ open(params: OpenParams): Promise<{
65
+ sessionId: string;
66
+ snapshot: SessionSnapshot;
67
+ }>;
68
+ send(sessionId: string, input: SendParams): Promise<{
69
+ inputId: string;
70
+ state: string;
71
+ }>;
72
+ interrupt(sessionId: string): Promise<{
73
+ turnId: string | null;
74
+ }>;
75
+ stopSubagents(sessionId: string, subagentIds?: string[]): Promise<{
76
+ stopped: string[];
77
+ }>;
78
+ /** Launch a sub-agent as a child session (delegated runtime). */
79
+ delegate(sessionId: string, params: DelegateParams): Promise<{
80
+ subagentId: string;
81
+ }>;
82
+ /** Wait for delegated sub-agents; never holds the session lock while waiting. */
83
+ waitSubagents(sessionId: string, subagentIds: string[] | undefined, timeoutMs: number): Promise<{
84
+ results: DelegatedResult[];
85
+ running: string[];
86
+ }>;
87
+ update(sessionId: string, changes: {
88
+ model?: string;
89
+ effort?: string;
90
+ systemPrompt?: string;
91
+ policy?: SessionPolicyInput;
92
+ }): Promise<{
93
+ outcome: 'applied' | 'deferred';
94
+ }>;
95
+ close(sessionId: string, reason: string): Promise<void>;
96
+ snapshot(sessionId: string): SessionSnapshot;
97
+ events(sessionId: string, afterSeq: number, limit?: number): {
98
+ events: SessionEventEnvelope[];
99
+ nextSeq: number;
100
+ hasMore: boolean;
101
+ };
102
+ list(state?: 'open' | 'closed' | 'all'): SessionSummary[];
103
+ stats(): {
104
+ sessions: number;
105
+ residentProcesses: number;
106
+ };
107
+ /** Retire every provider process; running work is recorded as interrupted. */
108
+ shutdown(): Promise<void>;
109
+ private resume;
110
+ private activate;
111
+ private session;
112
+ private configFrom;
113
+ private deps;
114
+ private requireFactory;
115
+ /** Keep-alive cap: retire least-recently-used idle processes; never busy ones. */
116
+ private enforceResidentCap;
117
+ private publish;
118
+ /** Serialize operations per session. */
119
+ private exclusive;
120
+ private assertRunning;
121
+ }