@peer-agent/runtime-sdk 0.0.1-beta.43

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 梁音
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @peer-agent/runtime-sdk
2
+
3
+ Public host-neutral orchestration SDK for embedding Peer Agent Runtime in a custom Node host.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @peer-agent/runtime-sdk
9
+ ```
10
+
11
+ The SDK depends on the matching public `@peer-agent/protocol` and `@peer-agent/runtime-core` versions.
12
+
13
+ ## Main APIs
14
+
15
+ ```js
16
+ import {
17
+ createRuntimeSdk,
18
+ createRuntimeSessionController,
19
+ createRuntimePipeline,
20
+ } from '@peer-agent/runtime-sdk';
21
+ ```
22
+
23
+ | API | Purpose |
24
+ |-----|---------|
25
+ | `createRuntimeSdk` | execute governed tool calls through a host port; emit runtime lifecycle events |
26
+ | `createRuntimeSessionController` | own session / turn / cancel / complete / fail state |
27
+ | `createRuntimePipeline` | sequence model turns, projected tool execution, and tool-result feedback |
28
+
29
+ ## Host boundary
30
+
31
+ Your host implements environment-specific ports (provider execution, authorization/blocking, optional hooks, model adapter and tool executor). The SDK does not require:
32
+
33
+ - Desktop / Electron
34
+ - `@peer-agent/cli` or the `peer` binary
35
+ - TUI / OpenTUI
36
+ - `@peer-agent/runtime-node`
37
+
38
+ A runnable external-host example lives at [`examples/external-host`](../../examples/external-host).
39
+
40
+ ## Product vs SDK
41
+
42
+ | Need | Install |
43
+ |------|---------|
44
+ | Run terminal product | `npm i -g @peer-agent/cli` |
45
+ | Embed runtime | `npm i @peer-agent/runtime-sdk` |
46
+ | Desktop GUI | Desktop release |
47
+
48
+ These are parallel product surfaces, not dependency chains.
49
+
50
+ ## Version policy
51
+
52
+ During beta, protocol / runtime-core / runtime-sdk follow the repository root `VERSION` and publish with the same npm dist-tag as CLI/Desktop releases.
@@ -0,0 +1,168 @@
1
+ import type { RuntimeDecision } from '@peer-agent/runtime-core';
2
+ import type { ContextAccountingSnapshot, RuntimeExecuteRequest, RuntimeExecutionContext, RuntimeToolCall, RuntimeToolResult } from '@peer-agent/protocol';
3
+ export type { RuntimeExecuteRequest, RuntimeExecutionContext, RuntimeToolCall, RuntimeToolResult, } from '@peer-agent/protocol';
4
+ export declare const RUNTIME_EVENT_PROTOCOL_VERSION: 1;
5
+ export type RuntimeEventProtocolVersion = typeof RUNTIME_EVENT_PROTOCOL_VERSION;
6
+ export type RuntimeSdkEventType = 'session.started' | 'message.delta' | 'reasoning.delta' | 'message.completed' | 'runtime.error' | 'tool.started' | 'hook.completed' | 'permission.requested' | 'permission.resolved' | 'tool.completed' | 'compaction.progress' | 'context.accounting';
7
+ export type RuntimeSdkToolCall = RuntimeToolCall;
8
+ export type RuntimeSdkExecuteRequest = RuntimeExecuteRequest;
9
+ export type RuntimeSdkExecutionContext = RuntimeExecutionContext;
10
+ export type RuntimeSdkToolResult = RuntimeToolResult;
11
+ export interface RuntimeSdkProviderExecution {
12
+ readonly result: RuntimeSdkToolResult;
13
+ readonly [key: string]: unknown;
14
+ }
15
+ export interface RuntimeSdkHookRecord {
16
+ readonly hookId?: string;
17
+ readonly decision?: RuntimeDecision;
18
+ readonly reason?: string;
19
+ readonly [key: string]: unknown;
20
+ }
21
+ export interface RuntimeSdkHookPayload {
22
+ readonly sessionId?: string;
23
+ readonly projectionId?: string;
24
+ readonly conversationId?: string;
25
+ readonly call: RuntimeSdkToolCall;
26
+ readonly result?: RuntimeSdkToolResult;
27
+ }
28
+ export interface RuntimeSdkHookRunner {
29
+ runPreToolUse?(payload: RuntimeSdkHookPayload): Promise<readonly RuntimeSdkHookRecord[]> | readonly RuntimeSdkHookRecord[];
30
+ runPostToolUse?(payload: RuntimeSdkHookPayload): Promise<readonly RuntimeSdkHookRecord[]> | readonly RuntimeSdkHookRecord[];
31
+ }
32
+ export interface RuntimeSdkPermissionRequest {
33
+ readonly kind: 'hook';
34
+ readonly hookEvent: 'PreToolUse';
35
+ readonly call: RuntimeSdkToolCall;
36
+ readonly toolCallId: string;
37
+ readonly capabilityId: string;
38
+ readonly args: unknown;
39
+ readonly workspacePath?: string;
40
+ readonly reason: string;
41
+ }
42
+ export interface RuntimeSdkApprovalDecision {
43
+ readonly decision: RuntimeDecision;
44
+ readonly [key: string]: unknown;
45
+ }
46
+ export interface RuntimeSdkApprovalPort {
47
+ requestApproval(request: RuntimeSdkPermissionRequest, context: RuntimeSdkExecutionContext): Promise<RuntimeSdkApprovalDecision> | RuntimeSdkApprovalDecision;
48
+ }
49
+ export interface RuntimeSdkHostAdapter {
50
+ executeProvider(request: RuntimeSdkExecuteRequest, context: RuntimeSdkExecutionContext): Promise<RuntimeSdkProviderExecution> | RuntimeSdkProviderExecution;
51
+ createBlockedExecution(options: {
52
+ readonly request: RuntimeSdkExecuteRequest;
53
+ readonly context: RuntimeSdkExecutionContext;
54
+ readonly decision: RuntimeDecision;
55
+ readonly reason: string;
56
+ readonly approval?: RuntimeSdkApprovalDecision;
57
+ }): RuntimeSdkProviderExecution;
58
+ appendHookEvidence?(result: RuntimeSdkToolResult, records: readonly RuntimeSdkHookRecord[], finalDecision: RuntimeDecision): RuntimeSdkToolResult;
59
+ hookRunner?: RuntimeSdkHookRunner | null;
60
+ approvalPort?: RuntimeSdkApprovalPort | null;
61
+ }
62
+ export interface RuntimeSdkEventBase {
63
+ readonly protocolVersion: RuntimeEventProtocolVersion;
64
+ readonly eventId: string;
65
+ readonly sequence: number;
66
+ readonly occurredAt: string;
67
+ readonly sessionId?: string;
68
+ readonly streamId?: string;
69
+ readonly projectionId?: string;
70
+ readonly conversationId?: string;
71
+ readonly toolCallId?: string;
72
+ readonly capabilityId?: string;
73
+ }
74
+ export interface RuntimeSdkSessionStartedEvent extends RuntimeSdkEventBase {
75
+ readonly type: 'session.started';
76
+ readonly sessionId: string;
77
+ readonly streamId?: string;
78
+ readonly mode?: string;
79
+ readonly providerId?: string;
80
+ readonly model?: string;
81
+ }
82
+ export interface RuntimeSdkMessageDeltaEvent extends RuntimeSdkEventBase {
83
+ readonly type: 'message.delta';
84
+ readonly streamId: string;
85
+ readonly content: string;
86
+ }
87
+ export interface RuntimeSdkReasoningDeltaEvent extends RuntimeSdkEventBase {
88
+ readonly type: 'reasoning.delta';
89
+ readonly streamId: string;
90
+ readonly content: string;
91
+ }
92
+ /**
93
+ * Mid-turn / automatic compaction progress for host UI surfaces.
94
+ * percent is 0-100; hosts should treat values below 100 as live progress.
95
+ */
96
+ export interface RuntimeSdkCompactionProgressEvent extends RuntimeSdkEventBase {
97
+ readonly type: 'compaction.progress';
98
+ readonly streamId: string;
99
+ readonly percent: number;
100
+ readonly reason?: 'preflight' | 'overflow' | 'manual' | string;
101
+ readonly phase?: 'started' | 'progress' | 'done';
102
+ readonly label?: string;
103
+ }
104
+ /** Shared Desktop/TUI context-capacity state. Presentation consumes this verbatim. */
105
+ export interface RuntimeSdkContextAccountingEvent extends RuntimeSdkEventBase {
106
+ readonly type: 'context.accounting';
107
+ readonly snapshot: ContextAccountingSnapshot;
108
+ }
109
+ export interface RuntimeSdkMessageCompletedEvent extends RuntimeSdkEventBase {
110
+ readonly type: 'message.completed';
111
+ readonly streamId: string;
112
+ readonly content?: string;
113
+ readonly usage?: unknown;
114
+ readonly lifetimeUsage?: unknown;
115
+ readonly finishReason?: string;
116
+ }
117
+ export interface RuntimeSdkRuntimeErrorEvent extends RuntimeSdkEventBase {
118
+ readonly type: 'runtime.error';
119
+ readonly code: string;
120
+ readonly message?: string;
121
+ readonly recoverable?: boolean;
122
+ readonly details?: unknown;
123
+ }
124
+ export interface RuntimeSdkToolStartedEvent extends RuntimeSdkEventBase {
125
+ readonly toolCallId: string;
126
+ readonly capabilityId: string;
127
+ readonly type: 'tool.started';
128
+ }
129
+ export interface RuntimeSdkHookCompletedEvent extends RuntimeSdkEventBase {
130
+ readonly toolCallId: string;
131
+ readonly capabilityId: string;
132
+ readonly type: 'hook.completed';
133
+ readonly phase: 'PreToolUse' | 'PostToolUse';
134
+ readonly decision: RuntimeDecision;
135
+ readonly records: readonly RuntimeSdkHookRecord[];
136
+ }
137
+ export interface RuntimeSdkPermissionRequestedEvent extends RuntimeSdkEventBase {
138
+ readonly toolCallId: string;
139
+ readonly capabilityId: string;
140
+ readonly type: 'permission.requested';
141
+ readonly decision: 'ask';
142
+ }
143
+ export interface RuntimeSdkPermissionResolvedEvent extends RuntimeSdkEventBase {
144
+ readonly toolCallId: string;
145
+ readonly capabilityId: string;
146
+ readonly type: 'permission.resolved';
147
+ readonly decision: RuntimeDecision;
148
+ }
149
+ export interface RuntimeSdkToolCompletedEvent extends RuntimeSdkEventBase {
150
+ readonly toolCallId: string;
151
+ readonly capabilityId: string;
152
+ readonly type: 'tool.completed';
153
+ readonly decision: RuntimeDecision;
154
+ readonly result: RuntimeSdkToolResult;
155
+ }
156
+ export type RuntimeSdkEvent = RuntimeSdkSessionStartedEvent | RuntimeSdkMessageDeltaEvent | RuntimeSdkReasoningDeltaEvent | RuntimeSdkCompactionProgressEvent | RuntimeSdkContextAccountingEvent | RuntimeSdkMessageCompletedEvent | RuntimeSdkRuntimeErrorEvent | RuntimeSdkToolStartedEvent | RuntimeSdkHookCompletedEvent | RuntimeSdkPermissionRequestedEvent | RuntimeSdkPermissionResolvedEvent | RuntimeSdkToolCompletedEvent;
157
+ export type RuntimeSdkEventInput = RuntimeSdkEvent extends infer Event ? Event extends RuntimeSdkEvent ? Omit<Event, 'protocolVersion' | 'eventId' | 'sequence' | 'occurredAt'> : never : never;
158
+ export type RuntimeSdkEventListener = (event: RuntimeSdkEvent) => void;
159
+ export interface RuntimeSdkOptions {
160
+ readonly host: RuntimeSdkHostAdapter;
161
+ readonly workspaceRoot?: string;
162
+ readonly now?: () => string;
163
+ }
164
+ export interface RuntimeSdk {
165
+ execute(request: RuntimeSdkExecuteRequest, context?: RuntimeSdkExecutionContext): Promise<RuntimeSdkProviderExecution>;
166
+ emit(event: RuntimeSdkEventInput): RuntimeSdkEvent;
167
+ subscribe(listener: RuntimeSdkEventListener): () => void;
168
+ }
@@ -0,0 +1 @@
1
+ export const RUNTIME_EVENT_PROTOCOL_VERSION = 1;
@@ -0,0 +1,73 @@
1
+ export type RuntimeGoalStatus = 'pending' | 'running' | 'paused' | 'blocked' | 'completed' | 'failed' | 'cancelled';
2
+ export type RuntimeGoalTaskStatus = 'pending' | 'running' | 'completed' | 'blocked' | 'failed' | 'cancelled';
3
+ export interface RuntimeGoalTaskInput {
4
+ readonly taskId: string;
5
+ readonly title: string;
6
+ }
7
+ export interface RuntimeGoalSuccessCriterion {
8
+ readonly description: string;
9
+ }
10
+ export interface RuntimeGoalTaskSnapshot extends RuntimeGoalTaskInput {
11
+ readonly status: RuntimeGoalTaskStatus;
12
+ readonly startedAt?: string;
13
+ readonly completedAt?: string;
14
+ readonly reason?: string;
15
+ readonly evidenceRefs: readonly string[];
16
+ }
17
+ export interface RuntimeGoalSnapshot {
18
+ readonly goalId: string;
19
+ readonly sourcePlanId: string;
20
+ readonly sessionId: string;
21
+ readonly title: string;
22
+ readonly goal: string;
23
+ readonly status: RuntimeGoalStatus;
24
+ readonly tasks: readonly RuntimeGoalTaskSnapshot[];
25
+ readonly successCriteria: readonly RuntimeGoalSuccessCriterion[];
26
+ readonly createdAt: string;
27
+ readonly updatedAt: string;
28
+ readonly reason?: string;
29
+ }
30
+ export interface RuntimeGoalCreateOptions {
31
+ readonly goalId: string;
32
+ readonly sourcePlanId: string;
33
+ readonly sessionId: string;
34
+ readonly title: string;
35
+ readonly goal: string;
36
+ readonly tasks: readonly RuntimeGoalTaskInput[];
37
+ readonly successCriteria?: readonly RuntimeGoalSuccessCriterion[];
38
+ }
39
+ export type RuntimeGoalTaskExecutionResult = {
40
+ readonly status: 'completed';
41
+ readonly evidenceRefs: readonly string[];
42
+ } | {
43
+ readonly status: 'blocked';
44
+ readonly reason: string;
45
+ readonly evidenceRefs?: readonly string[];
46
+ } | {
47
+ readonly status: 'failed';
48
+ readonly reason: string;
49
+ readonly evidenceRefs?: readonly string[];
50
+ };
51
+ export interface RuntimeGoalTaskExecutionContext {
52
+ readonly goalId: string;
53
+ readonly sourcePlanId: string;
54
+ readonly sessionId: string;
55
+ readonly taskIndex: number;
56
+ readonly signal: AbortSignal;
57
+ }
58
+ export type RuntimeGoalTaskExecutor = (task: RuntimeGoalTaskInput, context: RuntimeGoalTaskExecutionContext) => Promise<RuntimeGoalTaskExecutionResult>;
59
+ export interface RuntimeGoalControllerOptions {
60
+ readonly executeTask: RuntimeGoalTaskExecutor;
61
+ readonly now?: () => string;
62
+ readonly onChange?: (snapshot: RuntimeGoalSnapshot) => void;
63
+ }
64
+ export interface RuntimeGoalController {
65
+ create(options: RuntimeGoalCreateOptions): RuntimeGoalSnapshot;
66
+ start(goalId: string): Promise<RuntimeGoalSnapshot>;
67
+ pause(goalId: string): RuntimeGoalSnapshot;
68
+ resume(goalId: string): Promise<RuntimeGoalSnapshot>;
69
+ cancel(goalId: string, reason?: string): RuntimeGoalSnapshot;
70
+ get(goalId: string): RuntimeGoalSnapshot | null;
71
+ list(): readonly RuntimeGoalSnapshot[];
72
+ subscribe(listener: (snapshot: RuntimeGoalSnapshot) => void): () => void;
73
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { RuntimeGoalController, RuntimeGoalControllerOptions } from './goal-contracts.ts';
2
+ export declare function createRuntimeGoalController(options: RuntimeGoalControllerOptions): RuntimeGoalController;
@@ -0,0 +1,191 @@
1
+ export function createRuntimeGoalController(options) {
2
+ const now = options.now ?? (() => new Date().toISOString());
3
+ const goals = new Map();
4
+ const planGoals = new Map();
5
+ const listeners = new Set();
6
+ const publish = (record, snapshot) => {
7
+ record.snapshot = snapshot;
8
+ options.onChange?.(snapshot);
9
+ for (const listener of listeners)
10
+ listener(snapshot);
11
+ return snapshot;
12
+ };
13
+ const update = (record, change) => publish(record, {
14
+ ...record.snapshot,
15
+ ...change,
16
+ updatedAt: now(),
17
+ });
18
+ const requireGoal = (goalId) => {
19
+ const record = goals.get(goalId);
20
+ if (!record)
21
+ throw new Error(`Runtime goal ${goalId} does not exist.`);
22
+ return record;
23
+ };
24
+ const run = async (record) => {
25
+ if (record.run)
26
+ return record.run;
27
+ record.pauseRequested = false;
28
+ const controller = new AbortController();
29
+ record.controller = controller;
30
+ update(record, { status: 'running', reason: undefined });
31
+ const promise = (async () => {
32
+ try {
33
+ while (!controller.signal.aborted) {
34
+ if (record.pauseRequested)
35
+ return update(record, { status: 'paused' });
36
+ const taskIndex = record.snapshot.tasks.findIndex((task) => task.status === 'pending');
37
+ if (taskIndex < 0)
38
+ return update(record, { status: 'completed' });
39
+ const startedAt = now();
40
+ const runningTask = {
41
+ ...record.snapshot.tasks[taskIndex],
42
+ status: 'running',
43
+ startedAt,
44
+ };
45
+ const runningTasks = [...record.snapshot.tasks];
46
+ runningTasks[taskIndex] = runningTask;
47
+ update(record, { tasks: runningTasks });
48
+ const result = await options.executeTask(runningTask, {
49
+ goalId: record.snapshot.goalId,
50
+ sourcePlanId: record.snapshot.sourcePlanId,
51
+ sessionId: record.snapshot.sessionId,
52
+ taskIndex,
53
+ signal: controller.signal,
54
+ });
55
+ if (controller.signal.aborted) {
56
+ const cancelledTasks = record.snapshot.tasks.map((task, index) => index === taskIndex
57
+ ? { ...task, status: 'cancelled', completedAt: now(), reason: String(controller.signal.reason ?? 'cancelled') }
58
+ : task);
59
+ return update(record, {
60
+ status: 'cancelled',
61
+ tasks: cancelledTasks,
62
+ reason: String(controller.signal.reason ?? 'cancelled'),
63
+ });
64
+ }
65
+ if (result.status === 'completed' && result.evidenceRefs.length === 0) {
66
+ const failedTasks = [...record.snapshot.tasks];
67
+ failedTasks[taskIndex] = {
68
+ ...runningTask,
69
+ status: 'failed',
70
+ completedAt: now(),
71
+ reason: 'Task completion requires Evidence.',
72
+ evidenceRefs: [],
73
+ };
74
+ return update(record, {
75
+ status: 'failed',
76
+ tasks: failedTasks,
77
+ reason: 'Task completion requires Evidence.',
78
+ });
79
+ }
80
+ const tasks = [...record.snapshot.tasks];
81
+ tasks[taskIndex] = {
82
+ ...runningTask,
83
+ status: result.status,
84
+ completedAt: now(),
85
+ reason: result.status === 'completed' ? undefined : result.reason,
86
+ evidenceRefs: [...(result.evidenceRefs ?? [])],
87
+ };
88
+ if (result.status === 'blocked') {
89
+ return update(record, { status: 'blocked', tasks, reason: result.reason });
90
+ }
91
+ if (result.status === 'failed') {
92
+ return update(record, { status: 'failed', tasks, reason: result.reason });
93
+ }
94
+ update(record, { tasks });
95
+ }
96
+ return record.snapshot;
97
+ }
98
+ catch (error) {
99
+ if (controller.signal.aborted)
100
+ return record.snapshot;
101
+ const reason = error instanceof Error ? error.message : String(error);
102
+ const tasks = record.snapshot.tasks.map((task) => task.status === 'running'
103
+ ? { ...task, status: 'failed', completedAt: now(), reason }
104
+ : task);
105
+ return update(record, { status: 'failed', tasks, reason });
106
+ }
107
+ finally {
108
+ record.controller = undefined;
109
+ record.run = undefined;
110
+ }
111
+ })();
112
+ record.run = promise;
113
+ return promise;
114
+ };
115
+ return {
116
+ create(input) {
117
+ if (goals.has(input.goalId))
118
+ throw new Error(`Runtime goal ${input.goalId} already exists.`);
119
+ if (planGoals.has(input.sourcePlanId)) {
120
+ throw new Error(`Runtime plan ${input.sourcePlanId} already created goal ${planGoals.get(input.sourcePlanId)}.`);
121
+ }
122
+ if (input.tasks.length === 0)
123
+ throw new Error('Runtime goal requires at least one task.');
124
+ const createdAt = now();
125
+ const snapshot = {
126
+ goalId: input.goalId,
127
+ sourcePlanId: input.sourcePlanId,
128
+ sessionId: input.sessionId,
129
+ title: input.title,
130
+ goal: input.goal,
131
+ status: 'pending',
132
+ tasks: input.tasks.map((task) => ({ ...task, status: 'pending', evidenceRefs: [] })),
133
+ successCriteria: [...(input.successCriteria ?? [])],
134
+ createdAt,
135
+ updatedAt: createdAt,
136
+ };
137
+ const record = { snapshot, pauseRequested: false };
138
+ goals.set(input.goalId, record);
139
+ planGoals.set(input.sourcePlanId, input.goalId);
140
+ return publish(record, snapshot);
141
+ },
142
+ start(goalId) {
143
+ const record = requireGoal(goalId);
144
+ if (record.snapshot.status !== 'pending') {
145
+ throw new Error(`Runtime goal ${goalId} cannot start from ${record.snapshot.status}.`);
146
+ }
147
+ return run(record);
148
+ },
149
+ pause(goalId) {
150
+ const record = requireGoal(goalId);
151
+ if (record.snapshot.status !== 'running')
152
+ return record.snapshot;
153
+ record.pauseRequested = true;
154
+ return record.snapshot;
155
+ },
156
+ resume(goalId) {
157
+ const record = requireGoal(goalId);
158
+ if (record.snapshot.status === 'blocked') {
159
+ const tasks = record.snapshot.tasks.map((task) => task.status === 'blocked'
160
+ ? { ...task, status: 'pending', startedAt: undefined, completedAt: undefined, reason: undefined }
161
+ : task);
162
+ update(record, { status: 'paused', tasks, reason: undefined });
163
+ }
164
+ if (record.snapshot.status !== 'paused') {
165
+ throw new Error(`Runtime goal ${goalId} cannot resume from ${record.snapshot.status}.`);
166
+ }
167
+ return run(record);
168
+ },
169
+ cancel(goalId, reason = 'cancelled') {
170
+ const record = requireGoal(goalId);
171
+ const terminal = ['completed', 'failed', 'cancelled'];
172
+ if (terminal.includes(record.snapshot.status))
173
+ return record.snapshot;
174
+ record.controller?.abort(reason);
175
+ const tasks = record.snapshot.tasks.map((task) => task.status === 'pending' || task.status === 'running' || task.status === 'blocked'
176
+ ? { ...task, status: 'cancelled', completedAt: now(), reason }
177
+ : task);
178
+ return update(record, { status: 'cancelled', tasks, reason });
179
+ },
180
+ get(goalId) {
181
+ return goals.get(goalId)?.snapshot ?? null;
182
+ },
183
+ list() {
184
+ return [...goals.values()].map((record) => record.snapshot);
185
+ },
186
+ subscribe(listener) {
187
+ listeners.add(listener);
188
+ return () => listeners.delete(listener);
189
+ },
190
+ };
191
+ }
@@ -0,0 +1,9 @@
1
+ export { RUNTIME_EVENT_PROTOCOL_VERSION } from './contracts.ts';
2
+ export type { RuntimeEventProtocolVersion, RuntimeExecuteRequest, RuntimeExecutionContext, RuntimeToolCall, RuntimeToolResult, RuntimeSdk, RuntimeSdkApprovalDecision, RuntimeSdkApprovalPort, RuntimeSdkEvent, RuntimeSdkEventBase, RuntimeSdkEventInput, RuntimeSdkEventListener, RuntimeSdkEventType, RuntimeSdkSessionStartedEvent, RuntimeSdkMessageDeltaEvent, RuntimeSdkReasoningDeltaEvent, RuntimeSdkCompactionProgressEvent, RuntimeSdkContextAccountingEvent, RuntimeSdkMessageCompletedEvent, RuntimeSdkRuntimeErrorEvent, RuntimeSdkHookCompletedEvent, RuntimeSdkPermissionRequestedEvent, RuntimeSdkPermissionResolvedEvent, RuntimeSdkToolCompletedEvent, RuntimeSdkToolStartedEvent, RuntimeSdkExecuteRequest, RuntimeSdkExecutionContext, RuntimeSdkHookPayload, RuntimeSdkHookRecord, RuntimeSdkHookRunner, RuntimeSdkHostAdapter, RuntimeSdkOptions, RuntimeSdkPermissionRequest, RuntimeSdkProviderExecution, RuntimeSdkToolCall, RuntimeSdkToolResult, } from './contracts.ts';
3
+ export type { RuntimePipeline, RuntimePipelineCompletedTurn, RuntimePipelineContinueTurn, RuntimePipelineEventSink, RuntimePipelineLifecycleObserver, RuntimePipelineModelAdapter, RuntimePipelineOptions, RuntimePipelineRunInput, RuntimePipelineRunResult, RuntimePipelineStatus, RuntimePipelineToolCall, RuntimePipelineToolCallsTurn, RuntimePipelineToolExecution, RuntimePipelineToolExecutor, RuntimePipelineTurnContext, RuntimePipelineTurnResult, } from './pipeline-contracts.ts';
4
+ export { createRuntimePipeline } from './pipeline.ts';
5
+ export { createRuntimeSdk } from './runtime.ts';
6
+ export type { RuntimeSessionController, RuntimeSessionControllerOptions, RuntimeSessionResumeOptions, RuntimeSessionSnapshot, RuntimeSessionStartOptions, RuntimeSessionStatus, RuntimeSessionTurnHandle, RuntimeSessionTurnSnapshot, RuntimeTurnStatus, } from './session-contracts.ts';
7
+ export { createRuntimeSessionController } from './session-controller.ts';
8
+ export type { RuntimeGoalController, RuntimeGoalControllerOptions, RuntimeGoalCreateOptions, RuntimeGoalSnapshot, RuntimeGoalStatus, RuntimeGoalSuccessCriterion, RuntimeGoalTaskExecutionContext, RuntimeGoalTaskExecutionResult, RuntimeGoalTaskExecutor, RuntimeGoalTaskInput, RuntimeGoalTaskSnapshot, RuntimeGoalTaskStatus, } from './goal-contracts.ts';
9
+ export { createRuntimeGoalController } from './goal-controller.ts';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { RUNTIME_EVENT_PROTOCOL_VERSION } from "./contracts.js";
2
+ export { createRuntimePipeline } from "./pipeline.js";
3
+ export { createRuntimeSdk } from "./runtime.js";
4
+ export { createRuntimeSessionController } from "./session-controller.js";
5
+ export { createRuntimeGoalController } from "./goal-controller.js";
@@ -0,0 +1,92 @@
1
+ import type { RuntimeSdkEvent, RuntimeSdkEventInput, RuntimeSdkToolResult } from './contracts.ts';
2
+ export type RuntimePipelineStatus = 'completed' | 'stopped' | 'cancelled' | 'exhausted' | 'failed';
3
+ export interface RuntimePipelineRunInput<TInput = unknown> {
4
+ readonly sessionId: string;
5
+ readonly streamId?: string;
6
+ readonly conversationId?: string;
7
+ readonly mode?: string;
8
+ readonly providerId?: string;
9
+ readonly model?: string;
10
+ readonly input: TInput;
11
+ readonly maxTurns?: number;
12
+ }
13
+ export interface RuntimePipelineToolCall<TPayload = unknown> {
14
+ readonly toolCallId: string;
15
+ readonly capabilityId?: string;
16
+ readonly name?: string;
17
+ readonly arguments?: unknown;
18
+ readonly payload?: TPayload;
19
+ }
20
+ export interface RuntimePipelineToolExecution<TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TResult = RuntimeSdkToolResult> {
21
+ readonly call: TCall;
22
+ readonly result: TResult;
23
+ readonly terminal?: boolean;
24
+ readonly terminalReason?: string;
25
+ }
26
+ export interface RuntimePipelineTurnContext<TInput = unknown> {
27
+ readonly run: RuntimePipelineRunInput<TInput>;
28
+ readonly turn: number;
29
+ readonly signal?: AbortSignal;
30
+ readonly emit: (event: RuntimeSdkEventInput) => RuntimeSdkEvent | null;
31
+ }
32
+ export interface RuntimePipelineContinueTurn<TState> {
33
+ readonly kind: 'continue';
34
+ readonly state: TState;
35
+ }
36
+ export interface RuntimePipelineCompletedTurn<TState, TOutput = unknown> {
37
+ readonly kind: 'completed';
38
+ readonly state: TState;
39
+ readonly output?: TOutput;
40
+ readonly reason?: string;
41
+ }
42
+ export interface RuntimePipelineToolCallsTurn<TState, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall> {
43
+ readonly kind: 'tool_calls';
44
+ readonly state: TState;
45
+ readonly calls: readonly TCall[];
46
+ }
47
+ export type RuntimePipelineTurnResult<TState, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TOutput = unknown> = RuntimePipelineContinueTurn<TState> | RuntimePipelineCompletedTurn<TState, TOutput> | RuntimePipelineToolCallsTurn<TState, TCall>;
48
+ export interface RuntimePipelineModelAdapter<TInput = unknown, TState = unknown, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TToolResult = RuntimeSdkToolResult, TOutput = unknown> {
49
+ initialize(input: RuntimePipelineRunInput<TInput>, context: RuntimePipelineTurnContext<TInput>): TState | Promise<TState>;
50
+ runTurn(state: TState, context: RuntimePipelineTurnContext<TInput>): RuntimePipelineTurnResult<TState, TCall, TOutput> | Promise<RuntimePipelineTurnResult<TState, TCall, TOutput>>;
51
+ applyToolResults(state: TState, executions: readonly RuntimePipelineToolExecution<TCall, TToolResult>[], context: RuntimePipelineTurnContext<TInput>): TState | Promise<TState>;
52
+ onCompleted?(state: TState, output: TOutput | undefined, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
53
+ onStopped?(state: TState, executions: readonly RuntimePipelineToolExecution<TCall, TToolResult>[], context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
54
+ onCancelled?(state: TState | undefined, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
55
+ onExhausted?(state: TState, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
56
+ }
57
+ export interface RuntimePipelineToolExecutor<TInput = unknown, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TToolResult = RuntimeSdkToolResult> {
58
+ execute(call: TCall, context: RuntimePipelineTurnContext<TInput> & {
59
+ readonly index: number;
60
+ }): RuntimePipelineToolExecution<TCall, TToolResult> | Promise<RuntimePipelineToolExecution<TCall, TToolResult>>;
61
+ }
62
+ export interface RuntimePipelineEventSink {
63
+ emit(event: RuntimeSdkEventInput): RuntimeSdkEvent | null;
64
+ }
65
+ /**
66
+ * Observes stable Runtime Pipeline state transitions after the model adapter
67
+ * has committed them. Hosts use this seam for projections and presentation;
68
+ * observer failures never change pipeline execution.
69
+ */
70
+ export interface RuntimePipelineLifecycleObserver<TInput = unknown, TState = unknown, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TToolResult = RuntimeSdkToolResult> {
71
+ toolResultsApplied?(state: TState, executions: readonly RuntimePipelineToolExecution<TCall, TToolResult>[], context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
72
+ }
73
+ export interface RuntimePipelineOptions<TInput = unknown, TState = unknown, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TToolResult = RuntimeSdkToolResult, TOutput = unknown> {
74
+ readonly model: RuntimePipelineModelAdapter<TInput, TState, TCall, TToolResult, TOutput>;
75
+ readonly tools: RuntimePipelineToolExecutor<TInput, TCall, TToolResult>;
76
+ readonly events?: RuntimePipelineEventSink;
77
+ readonly lifecycle?: RuntimePipelineLifecycleObserver<TInput, TState, TCall, TToolResult>;
78
+ readonly defaultMaxTurns?: number;
79
+ }
80
+ export interface RuntimePipelineRunResult<TState = unknown, TOutput = unknown> {
81
+ readonly status: RuntimePipelineStatus;
82
+ readonly state?: TState;
83
+ readonly output?: TOutput;
84
+ readonly turns: number;
85
+ readonly toolCalls: number;
86
+ readonly reason?: string;
87
+ }
88
+ export interface RuntimePipeline<TInput = unknown, TState = unknown, TOutput = unknown> {
89
+ run(input: RuntimePipelineRunInput<TInput>, context?: {
90
+ readonly signal?: AbortSignal;
91
+ }): Promise<RuntimePipelineRunResult<TState, TOutput>>;
92
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { RuntimePipeline, RuntimePipelineOptions, RuntimePipelineToolCall } from './pipeline-contracts.ts';
2
+ export declare function createRuntimePipeline<TInput = unknown, TState = unknown, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TToolResult = unknown, TOutput = unknown>(options: RuntimePipelineOptions<TInput, TState, TCall, TToolResult, TOutput>): RuntimePipeline<TInput, TState, TOutput>;
@@ -0,0 +1,181 @@
1
+ const DEFAULT_MAX_TURNS = 64;
2
+ function normalizeMaxTurns(value, fallback) {
3
+ if (value === Number.POSITIVE_INFINITY)
4
+ return value;
5
+ if (!Number.isFinite(value))
6
+ return fallback;
7
+ return Math.max(1, Math.floor(value));
8
+ }
9
+ function isAbortError(error) {
10
+ return error instanceof Error && error.name === 'AbortError';
11
+ }
12
+ function errorMessage(error) {
13
+ if (error instanceof Error)
14
+ return error.message || error.name;
15
+ return String(error || 'runtime_pipeline_error');
16
+ }
17
+ function throwIfAborted(signal) {
18
+ if (signal?.aborted) {
19
+ const error = new Error('Aborted');
20
+ error.name = 'AbortError';
21
+ throw error;
22
+ }
23
+ }
24
+ export function createRuntimePipeline(options) {
25
+ if (!options?.model)
26
+ throw new Error('RuntimePipeline requires a model adapter.');
27
+ if (!options?.tools)
28
+ throw new Error('RuntimePipeline requires a tool executor.');
29
+ const defaultMaxTurns = normalizeMaxTurns(options.defaultMaxTurns, DEFAULT_MAX_TURNS);
30
+ return {
31
+ async run(input, context = {}) {
32
+ if (!input?.sessionId)
33
+ throw new Error('RuntimePipeline run requires sessionId.');
34
+ const signal = context.signal;
35
+ const maxTurns = normalizeMaxTurns(input.maxTurns, defaultMaxTurns);
36
+ let state;
37
+ let turns = 0;
38
+ let toolCalls = 0;
39
+ const emit = (event) => {
40
+ try {
41
+ return options.events?.emit(event) ?? null;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ };
47
+ const eventBase = {
48
+ sessionId: input.sessionId,
49
+ ...(input.streamId ? { streamId: input.streamId } : {}),
50
+ ...(input.conversationId ? { conversationId: input.conversationId } : {}),
51
+ };
52
+ const turnContext = (turn) => ({
53
+ run: input,
54
+ turn,
55
+ signal,
56
+ emit,
57
+ });
58
+ try {
59
+ throwIfAborted(signal);
60
+ emit({
61
+ type: 'session.started',
62
+ ...eventBase,
63
+ ...(input.mode ? { mode: input.mode } : {}),
64
+ ...(input.providerId ? { providerId: input.providerId } : {}),
65
+ ...(input.model ? { model: input.model } : {}),
66
+ });
67
+ state = await options.model.initialize(input, turnContext(0));
68
+ for (let turn = 0; turn < maxTurns; turn += 1) {
69
+ throwIfAborted(signal);
70
+ turns = turn + 1;
71
+ const currentContext = turnContext(turn);
72
+ const outcome = await options.model.runTurn(state, currentContext);
73
+ state = outcome.state;
74
+ if (outcome.kind === 'continue')
75
+ continue;
76
+ if (outcome.kind === 'completed') {
77
+ await options.model.onCompleted?.(state, outcome.output, currentContext);
78
+ if (input.streamId) {
79
+ emit({
80
+ type: 'message.completed',
81
+ ...eventBase,
82
+ streamId: input.streamId,
83
+ ...(outcome.reason ? { finishReason: outcome.reason } : {}),
84
+ });
85
+ }
86
+ return {
87
+ status: 'completed',
88
+ state,
89
+ output: outcome.output,
90
+ turns,
91
+ toolCalls,
92
+ ...(outcome.reason ? { reason: outcome.reason } : {}),
93
+ };
94
+ }
95
+ const executions = [];
96
+ for (const [index, call] of outcome.calls.entries()) {
97
+ throwIfAborted(signal);
98
+ const execution = await options.tools.execute(call, {
99
+ ...currentContext,
100
+ index,
101
+ });
102
+ executions.push(execution);
103
+ toolCalls += 1;
104
+ }
105
+ state = await options.model.applyToolResults(state, executions, currentContext);
106
+ try {
107
+ await options.lifecycle?.toolResultsApplied?.(state, executions, currentContext);
108
+ }
109
+ catch {
110
+ // Lifecycle observers publish projections/presentation only. Their
111
+ // failure must not change model or tool execution semantics.
112
+ }
113
+ const terminalExecution = executions.find((execution) => execution.terminal);
114
+ if (terminalExecution) {
115
+ await options.model.onStopped?.(state, executions, currentContext);
116
+ if (input.streamId) {
117
+ emit({
118
+ type: 'message.completed',
119
+ ...eventBase,
120
+ streamId: input.streamId,
121
+ finishReason: terminalExecution.terminalReason || 'tool_requested_stop',
122
+ });
123
+ }
124
+ return {
125
+ status: 'stopped',
126
+ state,
127
+ turns,
128
+ toolCalls,
129
+ reason: terminalExecution.terminalReason || 'tool_requested_stop',
130
+ };
131
+ }
132
+ }
133
+ const exhaustedContext = turnContext(turns);
134
+ await options.model.onExhausted?.(state, exhaustedContext);
135
+ const exhaustedReason = 'max_turns_exceeded';
136
+ emit({
137
+ type: 'runtime.error',
138
+ ...eventBase,
139
+ code: exhaustedReason,
140
+ message: exhaustedReason,
141
+ });
142
+ return {
143
+ status: 'exhausted',
144
+ state,
145
+ turns,
146
+ toolCalls,
147
+ reason: exhaustedReason,
148
+ };
149
+ }
150
+ catch (error) {
151
+ if (signal?.aborted || isAbortError(error)) {
152
+ const cancelledContext = turnContext(turns);
153
+ await options.model.onCancelled?.(state, cancelledContext);
154
+ return {
155
+ status: 'cancelled',
156
+ state,
157
+ turns,
158
+ toolCalls,
159
+ reason: 'aborted',
160
+ };
161
+ }
162
+ const detail = errorMessage(error);
163
+ emit({
164
+ type: 'runtime.error',
165
+ ...eventBase,
166
+ code: 'runtime_pipeline_error',
167
+ message: detail,
168
+ });
169
+ // Preserve partial state (already-executed tools / model messages) so
170
+ // hosts can recover instead of discarding progress on provider stream errors.
171
+ return {
172
+ status: 'failed',
173
+ state,
174
+ turns,
175
+ toolCalls,
176
+ reason: detail,
177
+ };
178
+ }
179
+ },
180
+ };
181
+ }
@@ -0,0 +1,2 @@
1
+ import { type RuntimeSdk, type RuntimeSdkOptions } from './contracts.ts';
2
+ export declare function createRuntimeSdk(options: RuntimeSdkOptions): RuntimeSdk;
@@ -0,0 +1,141 @@
1
+ import { mostRestrictiveHookDecision } from '@peer-agent/runtime-core';
2
+ import { RUNTIME_EVENT_PROTOCOL_VERSION, } from "./contracts.js";
3
+ function normalizeRecords(records) {
4
+ return Array.isArray(records) ? records : [];
5
+ }
6
+ function createRuntimeEventId(sequence) {
7
+ const randomId = globalThis.crypto?.randomUUID?.();
8
+ return randomId ? `runtime-event-${randomId}` : `runtime-event-${Date.now()}-${sequence}`;
9
+ }
10
+ export function createRuntimeSdk(options) {
11
+ if (!options?.host || typeof options.host.executeProvider !== 'function') {
12
+ throw new TypeError('Runtime SDK requires a host.executeProvider adapter.');
13
+ }
14
+ if (typeof options.host.createBlockedExecution !== 'function') {
15
+ throw new TypeError('Runtime SDK requires a host.createBlockedExecution adapter.');
16
+ }
17
+ const listeners = new Set();
18
+ const now = options.now ?? (() => new Date().toISOString());
19
+ let sequence = 0;
20
+ function emit(event) {
21
+ const nextSequence = ++sequence;
22
+ const nextEvent = {
23
+ ...event,
24
+ protocolVersion: RUNTIME_EVENT_PROTOCOL_VERSION,
25
+ eventId: createRuntimeEventId(nextSequence),
26
+ sequence: nextSequence,
27
+ occurredAt: now(),
28
+ };
29
+ for (const listener of listeners) {
30
+ try {
31
+ listener(nextEvent);
32
+ }
33
+ catch {
34
+ // Runtime events are observational. A broken subscriber must not interrupt execution.
35
+ }
36
+ }
37
+ return nextEvent;
38
+ }
39
+ function subscribe(listener) {
40
+ listeners.add(listener);
41
+ return () => listeners.delete(listener);
42
+ }
43
+ async function execute(request, context = {}) {
44
+ const call = request?.call;
45
+ if (!call?.toolCallId || !call?.capabilityId) {
46
+ throw new TypeError('Runtime SDK execute requires call.toolCallId and call.capabilityId.');
47
+ }
48
+ const eventBase = {
49
+ toolCallId: call.toolCallId,
50
+ capabilityId: call.capabilityId,
51
+ sessionId: request.sessionId,
52
+ projectionId: request.projectionId,
53
+ conversationId: request.conversationId,
54
+ };
55
+ const hookPayload = {
56
+ sessionId: request.sessionId,
57
+ projectionId: request.projectionId,
58
+ conversationId: request.conversationId,
59
+ call,
60
+ };
61
+ emit({ type: 'tool.started', ...eventBase });
62
+ const preRecords = normalizeRecords(options.host.hookRunner?.runPreToolUse
63
+ ? await options.host.hookRunner.runPreToolUse(hookPayload)
64
+ : []);
65
+ const preDecision = mostRestrictiveHookDecision(preRecords);
66
+ emit({
67
+ type: 'hook.completed',
68
+ ...eventBase,
69
+ phase: 'PreToolUse',
70
+ decision: preDecision,
71
+ records: preRecords,
72
+ });
73
+ if (preDecision === 'deny') {
74
+ const blocked = options.host.createBlockedExecution({
75
+ request,
76
+ context,
77
+ decision: 'deny',
78
+ reason: preRecords.find((record) => record.decision === 'deny')?.reason ?? 'hook_denied',
79
+ });
80
+ const result = options.host.appendHookEvidence
81
+ ? options.host.appendHookEvidence(blocked.result, preRecords, preDecision)
82
+ : blocked.result;
83
+ const resolvedBlocked = { ...blocked, result };
84
+ emit({ type: 'tool.completed', ...eventBase, decision: 'deny', result });
85
+ return resolvedBlocked;
86
+ }
87
+ if (preDecision === 'ask') {
88
+ const approvalRequest = {
89
+ kind: 'hook',
90
+ hookEvent: 'PreToolUse',
91
+ call,
92
+ toolCallId: call.toolCallId,
93
+ capabilityId: call.capabilityId,
94
+ args: call.arguments ?? call.argumentsPreview ?? {},
95
+ workspacePath: context.workspaceRoot ?? options.workspaceRoot,
96
+ reason: preRecords.find((record) => record.decision === 'ask')?.reason ?? 'hook_approval_required',
97
+ };
98
+ emit({ type: 'permission.requested', ...eventBase, decision: 'ask' });
99
+ const approval = options.host.approvalPort
100
+ ? await options.host.approvalPort.requestApproval(approvalRequest, context)
101
+ : { decision: 'ask' };
102
+ emit({ type: 'permission.resolved', ...eventBase, decision: approval.decision });
103
+ if (approval.decision !== 'allow') {
104
+ const blocked = options.host.createBlockedExecution({
105
+ request,
106
+ context,
107
+ decision: approval.decision,
108
+ reason: approval.decision === 'deny' ? 'hook_approval_denied' : 'hook_approval_required',
109
+ approval,
110
+ });
111
+ const result = options.host.appendHookEvidence
112
+ ? options.host.appendHookEvidence(blocked.result, preRecords, preDecision)
113
+ : blocked.result;
114
+ const resolvedBlocked = { ...blocked, result };
115
+ emit({ type: 'tool.completed', ...eventBase, decision: approval.decision, result });
116
+ return resolvedBlocked;
117
+ }
118
+ }
119
+ const execution = await options.host.executeProvider(request, context);
120
+ const postRecords = normalizeRecords(options.host.hookRunner?.runPostToolUse
121
+ ? await options.host.hookRunner.runPostToolUse({
122
+ ...hookPayload,
123
+ result: execution.result,
124
+ })
125
+ : []);
126
+ emit({
127
+ type: 'hook.completed',
128
+ ...eventBase,
129
+ phase: 'PostToolUse',
130
+ decision: mostRestrictiveHookDecision(postRecords),
131
+ records: postRecords,
132
+ });
133
+ const result = options.host.appendHookEvidence
134
+ ? options.host.appendHookEvidence(execution.result, [...preRecords, ...postRecords], preDecision)
135
+ : execution.result;
136
+ const resolvedExecution = { ...execution, result };
137
+ emit({ type: 'tool.completed', ...eventBase, decision: preDecision, result });
138
+ return resolvedExecution;
139
+ }
140
+ return { emit, execute, subscribe };
141
+ }
@@ -0,0 +1,57 @@
1
+ export type RuntimeSessionStatus = 'idle' | 'running';
2
+ export type RuntimeTurnStatus = 'running' | 'completed' | 'failed' | 'cancelled';
3
+ export interface RuntimeSessionTurnSnapshot {
4
+ readonly turnId: string;
5
+ readonly turnIndex: number;
6
+ readonly streamId?: string;
7
+ readonly status: RuntimeTurnStatus;
8
+ readonly startedAt: string;
9
+ readonly completedAt?: string;
10
+ readonly reason?: string;
11
+ }
12
+ export interface RuntimeSessionSnapshot {
13
+ readonly sessionId: string;
14
+ readonly conversationId?: string;
15
+ readonly status: RuntimeSessionStatus;
16
+ readonly createdAt: string;
17
+ readonly updatedAt: string;
18
+ readonly nextTurnIndex: number;
19
+ readonly activeTurn?: RuntimeSessionTurnSnapshot;
20
+ readonly lastTurn?: RuntimeSessionTurnSnapshot;
21
+ }
22
+ export interface RuntimeSessionStartOptions {
23
+ readonly sessionId: string;
24
+ readonly conversationId?: string;
25
+ readonly streamId?: string;
26
+ readonly signal?: AbortSignal;
27
+ }
28
+ export interface RuntimeSessionResumeOptions {
29
+ readonly sessionId: string;
30
+ readonly streamId?: string;
31
+ readonly signal?: AbortSignal;
32
+ }
33
+ export interface RuntimeSessionTurnHandle {
34
+ readonly sessionId: string;
35
+ readonly conversationId?: string;
36
+ readonly turnId: string;
37
+ readonly turnIndex: number;
38
+ readonly streamId?: string;
39
+ readonly signal: AbortSignal;
40
+ complete(): RuntimeSessionSnapshot;
41
+ fail(reason?: string): RuntimeSessionSnapshot;
42
+ cancel(reason?: string): RuntimeSessionSnapshot;
43
+ snapshot(): RuntimeSessionSnapshot;
44
+ }
45
+ export interface RuntimeSessionControllerOptions {
46
+ readonly createTurnId?: (sessionId: string, turnIndex: number) => string;
47
+ readonly now?: () => string;
48
+ readonly onChange?: (snapshot: RuntimeSessionSnapshot) => void;
49
+ }
50
+ export interface RuntimeSessionController {
51
+ start(options: RuntimeSessionStartOptions): RuntimeSessionTurnHandle;
52
+ resume(options: RuntimeSessionResumeOptions): RuntimeSessionTurnHandle;
53
+ cancel(sessionId: string, reason?: string): RuntimeSessionSnapshot | null;
54
+ get(sessionId: string): RuntimeSessionSnapshot | null;
55
+ list(): readonly RuntimeSessionSnapshot[];
56
+ delete(sessionId: string): boolean;
57
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { RuntimeSessionController, RuntimeSessionControllerOptions } from './session-contracts.ts';
2
+ export declare function createRuntimeSessionController(options?: RuntimeSessionControllerOptions): RuntimeSessionController;
@@ -0,0 +1,138 @@
1
+ function normalizeReason(reason, fallback = 'aborted') {
2
+ return typeof reason === 'string' && reason.trim() ? reason.trim() : fallback;
3
+ }
4
+ function snapshotSession(record) {
5
+ return {
6
+ sessionId: record.sessionId,
7
+ ...(record.conversationId ? { conversationId: record.conversationId } : {}),
8
+ status: record.activeTurn ? 'running' : 'idle',
9
+ createdAt: record.createdAt,
10
+ updatedAt: record.updatedAt,
11
+ nextTurnIndex: record.nextTurnIndex,
12
+ ...(record.activeTurn ? { activeTurn: { ...record.activeTurn.snapshot } } : {}),
13
+ ...(record.lastTurn ? { lastTurn: { ...record.lastTurn } } : {}),
14
+ };
15
+ }
16
+ export function createRuntimeSessionController(options = {}) {
17
+ const sessions = new Map();
18
+ const now = options.now ?? (() => new Date().toISOString());
19
+ const createTurnId = options.createTurnId
20
+ ?? ((sessionId, turnIndex) => `${sessionId}:turn:${turnIndex}`);
21
+ const publish = (record) => {
22
+ const snapshot = snapshotSession(record);
23
+ options.onChange?.(snapshot);
24
+ return snapshot;
25
+ };
26
+ const finishTurn = (record, activeTurn, status, reason, abortSignal = false) => {
27
+ if (record.activeTurn !== activeTurn)
28
+ return snapshotSession(record);
29
+ activeTurn.detachExternalSignal?.();
30
+ if (abortSignal && !activeTurn.controller.signal.aborted) {
31
+ activeTurn.controller.abort(reason);
32
+ }
33
+ const completedAt = now();
34
+ record.activeTurn = undefined;
35
+ record.lastTurn = {
36
+ ...activeTurn.snapshot,
37
+ status,
38
+ completedAt,
39
+ ...(reason ? { reason } : {}),
40
+ };
41
+ record.updatedAt = completedAt;
42
+ return publish(record);
43
+ };
44
+ const beginTurn = (record, options, startedAt = now()) => {
45
+ if (record.activeTurn) {
46
+ throw new Error(`Runtime session ${record.sessionId} already has an active turn`);
47
+ }
48
+ const turnIndex = record.nextTurnIndex;
49
+ const turnId = createTurnId(record.sessionId, turnIndex);
50
+ const controller = new AbortController();
51
+ const turnSnapshot = {
52
+ turnId,
53
+ turnIndex,
54
+ ...(options.streamId ? { streamId: options.streamId } : {}),
55
+ status: 'running',
56
+ startedAt,
57
+ };
58
+ const activeTurn = { snapshot: turnSnapshot, controller };
59
+ record.activeTurn = activeTurn;
60
+ record.nextTurnIndex = turnIndex + 1;
61
+ record.updatedAt = startedAt;
62
+ const cancelFromExternalSignal = () => {
63
+ finishTurn(record, activeTurn, 'cancelled', normalizeReason(options.signal?.reason, 'host_aborted'), true);
64
+ };
65
+ if (options.signal) {
66
+ if (options.signal.aborted) {
67
+ cancelFromExternalSignal();
68
+ }
69
+ else {
70
+ options.signal.addEventListener('abort', cancelFromExternalSignal, { once: true });
71
+ activeTurn.detachExternalSignal = () => {
72
+ options.signal?.removeEventListener('abort', cancelFromExternalSignal);
73
+ };
74
+ }
75
+ }
76
+ if (record.activeTurn === activeTurn)
77
+ publish(record);
78
+ return {
79
+ sessionId: record.sessionId,
80
+ ...(record.conversationId ? { conversationId: record.conversationId } : {}),
81
+ turnId,
82
+ turnIndex,
83
+ ...(options.streamId ? { streamId: options.streamId } : {}),
84
+ signal: controller.signal,
85
+ complete: () => finishTurn(record, activeTurn, 'completed'),
86
+ fail: (reason) => finishTurn(record, activeTurn, 'failed', reason, true),
87
+ cancel: (reason) => finishTurn(record, activeTurn, 'cancelled', normalizeReason(reason), true),
88
+ snapshot: () => snapshotSession(record),
89
+ };
90
+ };
91
+ return {
92
+ start(startOptions) {
93
+ if (sessions.has(startOptions.sessionId)) {
94
+ throw new Error(`Runtime session ${startOptions.sessionId} already exists`);
95
+ }
96
+ const startedAt = now();
97
+ const record = {
98
+ sessionId: startOptions.sessionId,
99
+ ...(startOptions.conversationId
100
+ ? { conversationId: startOptions.conversationId }
101
+ : {}),
102
+ createdAt: startedAt,
103
+ updatedAt: startedAt,
104
+ nextTurnIndex: 0,
105
+ };
106
+ sessions.set(record.sessionId, record);
107
+ return beginTurn(record, startOptions, startedAt);
108
+ },
109
+ resume(resumeOptions) {
110
+ const record = sessions.get(resumeOptions.sessionId);
111
+ if (!record) {
112
+ throw new Error(`Runtime session ${resumeOptions.sessionId} does not exist`);
113
+ }
114
+ return beginTurn(record, resumeOptions);
115
+ },
116
+ cancel(sessionId, reason = 'aborted') {
117
+ const record = sessions.get(sessionId);
118
+ if (!record)
119
+ return null;
120
+ if (!record.activeTurn)
121
+ return snapshotSession(record);
122
+ return finishTurn(record, record.activeTurn, 'cancelled', normalizeReason(reason), true);
123
+ },
124
+ get(sessionId) {
125
+ const record = sessions.get(sessionId);
126
+ return record ? snapshotSession(record) : null;
127
+ },
128
+ list() {
129
+ return [...sessions.values()].map(snapshotSession);
130
+ },
131
+ delete(sessionId) {
132
+ const record = sessions.get(sessionId);
133
+ if (!record || record.activeTurn)
134
+ return false;
135
+ return sessions.delete(sessionId);
136
+ },
137
+ };
138
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@peer-agent/runtime-sdk",
3
+ "version": "0.0.1-beta.43",
4
+ "description": "Public host-neutral orchestration SDK for Peer Agent runtimes.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/yinLiangDream/Peer-Agent.git",
14
+ "directory": "packages/runtime-sdk"
15
+ },
16
+ "homepage": "https://github.com/yinLiangDream/Peer-Agent#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/yinLiangDream/Peer-Agent/issues"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "dependencies": {
37
+ "@peer-agent/protocol": "^0.0.1-beta.43",
38
+ "@peer-agent/runtime-core": "^0.0.1-beta.43"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.build.json",
45
+ "test": "node --test src/*.test.ts",
46
+ "typecheck": "tsc -p tsconfig.json"
47
+ }
48
+ }