@peer-agent/runtime-sdk 0.1.0-rc.3 → 0.1.0-rc.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import type { ContextAccountingSnapshot, RuntimeExecuteRequest, RuntimeExecution
3
3
  export type { RuntimeExecuteRequest, RuntimeExecutionContext, RuntimeToolCall, RuntimeToolResult, } from '@peer-agent/protocol';
4
4
  export declare const RUNTIME_EVENT_PROTOCOL_VERSION: 1;
5
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';
6
+ export type RuntimeSdkEventType = 'session.started' | 'message.delta' | 'reasoning.delta' | 'stream.discard' | 'message.completed' | 'runtime.error' | 'tool.started' | 'hook.completed' | 'permission.requested' | 'permission.resolved' | 'tool.completed' | 'compaction.progress' | 'context.accounting';
7
7
  export type RuntimeSdkToolCall = RuntimeToolCall;
8
8
  export type RuntimeSdkExecuteRequest = RuntimeExecuteRequest;
9
9
  export type RuntimeSdkExecutionContext = RuntimeExecutionContext;
@@ -93,6 +93,12 @@ export interface RuntimeSdkReasoningDeltaEvent extends RuntimeSdkEventBase {
93
93
  * Mid-turn / automatic compaction progress for host UI surfaces.
94
94
  * percent is 0-100; hosts should treat values below 100 as live progress.
95
95
  */
96
+ export interface RuntimeSdkStreamDiscardEvent extends RuntimeSdkEventBase {
97
+ readonly type: 'stream.discard';
98
+ readonly streamId: string;
99
+ readonly textChars: number;
100
+ readonly reasoningChars: number;
101
+ }
96
102
  export interface RuntimeSdkCompactionProgressEvent extends RuntimeSdkEventBase {
97
103
  readonly type: 'compaction.progress';
98
104
  readonly streamId: string;
@@ -153,7 +159,7 @@ export interface RuntimeSdkToolCompletedEvent extends RuntimeSdkEventBase {
153
159
  readonly decision: RuntimeDecision;
154
160
  readonly result: RuntimeSdkToolResult;
155
161
  }
156
- export type RuntimeSdkEvent = RuntimeSdkSessionStartedEvent | RuntimeSdkMessageDeltaEvent | RuntimeSdkReasoningDeltaEvent | RuntimeSdkCompactionProgressEvent | RuntimeSdkContextAccountingEvent | RuntimeSdkMessageCompletedEvent | RuntimeSdkRuntimeErrorEvent | RuntimeSdkToolStartedEvent | RuntimeSdkHookCompletedEvent | RuntimeSdkPermissionRequestedEvent | RuntimeSdkPermissionResolvedEvent | RuntimeSdkToolCompletedEvent;
162
+ export type RuntimeSdkEvent = RuntimeSdkSessionStartedEvent | RuntimeSdkMessageDeltaEvent | RuntimeSdkReasoningDeltaEvent | RuntimeSdkStreamDiscardEvent | RuntimeSdkCompactionProgressEvent | RuntimeSdkContextAccountingEvent | RuntimeSdkMessageCompletedEvent | RuntimeSdkRuntimeErrorEvent | RuntimeSdkToolStartedEvent | RuntimeSdkHookCompletedEvent | RuntimeSdkPermissionRequestedEvent | RuntimeSdkPermissionResolvedEvent | RuntimeSdkToolCompletedEvent;
157
163
  export type RuntimeSdkEventInput = RuntimeSdkEvent extends infer Event ? Event extends RuntimeSdkEvent ? Omit<Event, 'protocolVersion' | 'eventId' | 'sequence' | 'occurredAt'> : never : never;
158
164
  export type RuntimeSdkEventListener = (event: RuntimeSdkEvent) => void;
159
165
  export interface RuntimeSdkOptions {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
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';
2
+ export type { RuntimeEventProtocolVersion, RuntimeExecuteRequest, RuntimeExecutionContext, RuntimeToolCall, RuntimeToolResult, RuntimeSdk, RuntimeSdkApprovalDecision, RuntimeSdkApprovalPort, RuntimeSdkEvent, RuntimeSdkEventBase, RuntimeSdkEventInput, RuntimeSdkEventListener, RuntimeSdkEventType, RuntimeSdkSessionStartedEvent, RuntimeSdkMessageDeltaEvent, RuntimeSdkReasoningDeltaEvent, RuntimeSdkStreamDiscardEvent, RuntimeSdkCompactionProgressEvent, RuntimeSdkContextAccountingEvent, RuntimeSdkMessageCompletedEvent, RuntimeSdkRuntimeErrorEvent, RuntimeSdkHookCompletedEvent, RuntimeSdkPermissionRequestedEvent, RuntimeSdkPermissionResolvedEvent, RuntimeSdkToolCompletedEvent, RuntimeSdkToolStartedEvent, RuntimeSdkExecuteRequest, RuntimeSdkExecutionContext, RuntimeSdkHookPayload, RuntimeSdkHookRecord, RuntimeSdkHookRunner, RuntimeSdkHostAdapter, RuntimeSdkOptions, RuntimeSdkPermissionRequest, RuntimeSdkProviderExecution, RuntimeSdkToolCall, RuntimeSdkToolResult, } from './contracts.ts';
3
3
  export type { RuntimePipeline, RuntimePipelineCompletedTurn, RuntimePipelineContinueTurn, RuntimePipelineEventSink, RuntimePipelineLifecycleObserver, RuntimePipelineModelAdapter, RuntimePipelineOptions, RuntimePipelineRunInput, RuntimePipelineRunResult, RuntimePipelineStatus, RuntimePipelineToolCall, RuntimePipelineToolCallsTurn, RuntimePipelineToolExecution, RuntimePipelineToolExecutor, RuntimePipelineTurnContext, RuntimePipelineTurnResult, } from './pipeline-contracts.ts';
4
4
  export { createRuntimePipeline } from './pipeline.ts';
5
5
  export { createRuntimeSdk } from './runtime.ts';
@@ -1,5 +1,5 @@
1
1
  import type { RuntimeSdkEvent, RuntimeSdkEventInput, RuntimeSdkToolResult } from './contracts.ts';
2
- export type RuntimePipelineStatus = 'completed' | 'stopped' | 'cancelled' | 'exhausted' | 'failed';
2
+ export type RuntimePipelineStatus = 'completed' | 'stopped' | 'cancelled' | 'yielded' | 'exhausted' | 'failed';
3
3
  export interface RuntimePipelineRunInput<TInput = unknown> {
4
4
  readonly sessionId: string;
5
5
  readonly streamId?: string;
@@ -9,6 +9,13 @@ export interface RuntimePipelineRunInput<TInput = unknown> {
9
9
  readonly model?: string;
10
10
  readonly input: TInput;
11
11
  readonly maxTurns?: number;
12
+ readonly maxToolCalls?: number;
13
+ /** Scheduling allowance; unlike maxToolCalls this can end at a batch boundary. */
14
+ readonly sliceToolCalls?: number;
15
+ readonly yieldAtTurnLimit?: boolean;
16
+ /** Host scheduling request, checked at durable batch boundaries. */
17
+ readonly shouldYield?: () => boolean;
18
+ readonly maxToolBatchCalls?: number;
12
19
  }
13
20
  export interface RuntimePipelineToolCall<TPayload = unknown> {
14
21
  readonly toolCallId: string;
@@ -49,12 +56,17 @@ export interface RuntimePipelineModelAdapter<TInput = unknown, TState = unknown,
49
56
  initialize(input: RuntimePipelineRunInput<TInput>, context: RuntimePipelineTurnContext<TInput>): TState | Promise<TState>;
50
57
  runTurn(state: TState, context: RuntimePipelineTurnContext<TInput>): RuntimePipelineTurnResult<TState, TCall, TOutput> | Promise<RuntimePipelineTurnResult<TState, TCall, TOutput>>;
51
58
  applyToolResults(state: TState, executions: readonly RuntimePipelineToolExecution<TCall, TToolResult>[], context: RuntimePipelineTurnContext<TInput>): TState | Promise<TState>;
59
+ /** Critical durable commit after paired tool results; failure ends execution. */
60
+ checkpoint?(state: TState, executions: readonly RuntimePipelineToolExecution<TCall, TToolResult>[], context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
52
61
  onCompleted?(state: TState, output: TOutput | undefined, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
53
62
  onStopped?(state: TState, executions: readonly RuntimePipelineToolExecution<TCall, TToolResult>[], context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
54
63
  onCancelled?(state: TState | undefined, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
55
- onExhausted?(state: TState, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
64
+ onYield?(state: TState, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
65
+ onExhausted?(state: TState, context: RuntimePipelineTurnContext<TInput>, reason: 'max_turns_exceeded' | 'max_tool_calls_exceeded'): void | Promise<void>;
56
66
  }
57
67
  export interface RuntimePipelineToolExecutor<TInput = unknown, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TToolResult = RuntimeSdkToolResult> {
68
+ /** Build a paired, explicit non-execution result without dispatching a capability. */
69
+ notExecuted?(call: TCall, reason: string): RuntimePipelineToolExecution<TCall, TToolResult> | Promise<RuntimePipelineToolExecution<TCall, TToolResult>>;
58
70
  execute(call: TCall, context: RuntimePipelineTurnContext<TInput> & {
59
71
  readonly index: number;
60
72
  }): RuntimePipelineToolExecution<TCall, TToolResult> | Promise<RuntimePipelineToolExecution<TCall, TToolResult>>;
package/dist/pipeline.js CHANGED
@@ -34,6 +34,10 @@ export function createRuntimePipeline(options) {
34
34
  throw new Error('RuntimePipeline run requires sessionId.');
35
35
  const signal = context.signal;
36
36
  const maxTurns = normalizeMaxTurns(input.maxTurns, defaultMaxTurns);
37
+ const maxToolCalls = Number.isFinite(input.maxToolCalls)
38
+ ? Math.max(0, Math.floor(input.maxToolCalls)) : Number.POSITIVE_INFINITY;
39
+ const sliceToolCalls = Number.isFinite(input.sliceToolCalls) ? Math.max(1, input.sliceToolCalls) : Infinity;
40
+ const maxBatch = Number.isFinite(input.maxToolBatchCalls) ? Math.max(1, input.maxToolBatchCalls) : Infinity;
37
41
  let state;
38
42
  let turns = 0;
39
43
  let toolCalls = 0;
@@ -56,6 +60,15 @@ export function createRuntimePipeline(options) {
56
60
  signal,
57
61
  emit,
58
62
  });
63
+ const exhaust = async (reason) => {
64
+ await options.model.onExhausted?.(state, turnContext(turns), reason);
65
+ emit({ type: 'runtime.error', ...eventBase, code: reason, message: reason });
66
+ return { status: 'exhausted', state, turns, toolCalls, reason };
67
+ };
68
+ const yieldRun = async () => {
69
+ await options.model.onYield?.(state, turnContext(turns));
70
+ return { status: 'yielded', state, turns, toolCalls, reason: 'scheduling_slice' };
71
+ };
59
72
  try {
60
73
  throwIfAborted(signal);
61
74
  emit({
@@ -68,12 +81,17 @@ export function createRuntimePipeline(options) {
68
81
  state = await options.model.initialize(input, turnContext(0));
69
82
  for (let turn = 0; turn < maxTurns; turn += 1) {
70
83
  throwIfAborted(signal);
84
+ if (input.shouldYield?.() === true)
85
+ return await yieldRun();
71
86
  turns = turn + 1;
72
87
  const currentContext = turnContext(turn);
73
88
  const outcome = await options.model.runTurn(state, currentContext);
74
89
  state = outcome.state;
75
- if (outcome.kind === 'continue')
90
+ if (outcome.kind === 'continue') {
91
+ if (input.shouldYield?.() === true)
92
+ return await yieldRun();
76
93
  continue;
94
+ }
77
95
  if (outcome.kind === 'completed') {
78
96
  await options.model.onCompleted?.(state, outcome.output, currentContext);
79
97
  if (input.streamId) {
@@ -93,30 +111,65 @@ export function createRuntimePipeline(options) {
93
111
  ...(outcome.reason ? { reason: outcome.reason } : {}),
94
112
  };
95
113
  }
114
+ // Reserve the whole batch before dispatch. Never execute a partial
115
+ // write batch or exceed a caller's remaining budget inside one turn.
116
+ if (outcome.calls.length > maxBatch)
117
+ throw new Error('tool_batch_too_large');
118
+ if (toolCalls + outcome.calls.length > maxToolCalls) {
119
+ if (options.tools.notExecuted) {
120
+ const denied = await Promise.all(outcome.calls.map(call => options.tools.notExecuted(call, 'max_tool_calls_exceeded')));
121
+ state = await options.model.applyToolResults(state, denied, currentContext);
122
+ await options.model.checkpoint?.(state, denied, currentContext);
123
+ }
124
+ return await exhaust('max_tool_calls_exceeded');
125
+ }
96
126
  const executions = [];
97
127
  if (isParallelSafeLocalToolBatch(outcome.calls)) {
98
128
  const batch = await Promise.all(outcome.calls.map(async (call, index) => {
129
+ if (signal?.aborted && options.tools.notExecuted)
130
+ return options.tools.notExecuted(call, 'cancelled');
99
131
  throwIfAborted(signal);
100
- return options.tools.execute(call, {
101
- ...currentContext,
102
- index,
103
- });
132
+ try {
133
+ return await options.tools.execute(call, { ...currentContext, index });
134
+ }
135
+ catch (error) {
136
+ if (!options.tools.notExecuted)
137
+ throw error;
138
+ return options.tools.notExecuted(call, isAbortError(error) ? 'cancelled_outcome_unknown' : 'execution_outcome_unknown');
139
+ }
104
140
  }));
105
141
  executions.push(...batch);
106
142
  toolCalls += batch.length;
107
143
  }
108
144
  else {
109
145
  for (const [index, call] of outcome.calls.entries()) {
110
- throwIfAborted(signal);
111
- const execution = await options.tools.execute(call, {
112
- ...currentContext,
113
- index,
114
- });
146
+ let execution;
147
+ if (signal?.aborted && options.tools.notExecuted)
148
+ execution = await options.tools.notExecuted(call, 'cancelled');
149
+ else {
150
+ throwIfAborted(signal);
151
+ try {
152
+ execution = await options.tools.execute(call, { ...currentContext, index });
153
+ }
154
+ catch (error) {
155
+ if (!options.tools.notExecuted)
156
+ throw error;
157
+ execution = await options.tools.notExecuted(call, isAbortError(error) ? 'cancelled_outcome_unknown' : 'execution_outcome_unknown');
158
+ }
159
+ }
115
160
  executions.push(execution);
116
161
  toolCalls += 1;
162
+ if (execution.terminal && options.tools.notExecuted) {
163
+ for (const remaining of outcome.calls.slice(index + 1))
164
+ executions.push(await options.tools.notExecuted(remaining, 'batch_stopped'));
165
+ break;
166
+ }
117
167
  }
118
168
  }
119
169
  state = await options.model.applyToolResults(state, executions, currentContext);
170
+ // Completed results remain durable even when cancellation arrived during execution.
171
+ await options.model.checkpoint?.(state, executions, currentContext);
172
+ throwIfAborted(signal);
120
173
  try {
121
174
  await options.lifecycle?.toolResultsApplied?.(state, executions, currentContext);
122
175
  }
@@ -143,23 +196,10 @@ export function createRuntimePipeline(options) {
143
196
  reason: terminalExecution.terminalReason || 'tool_requested_stop',
144
197
  };
145
198
  }
199
+ if (toolCalls >= sliceToolCalls || input.shouldYield?.() === true)
200
+ return await yieldRun();
146
201
  }
147
- const exhaustedContext = turnContext(turns);
148
- await options.model.onExhausted?.(state, exhaustedContext);
149
- const exhaustedReason = 'max_turns_exceeded';
150
- emit({
151
- type: 'runtime.error',
152
- ...eventBase,
153
- code: exhaustedReason,
154
- message: exhaustedReason,
155
- });
156
- return {
157
- status: 'exhausted',
158
- state,
159
- turns,
160
- toolCalls,
161
- reason: exhaustedReason,
162
- };
202
+ return input.yieldAtTurnLimit ? await yieldRun() : await exhaust('max_turns_exceeded');
163
203
  }
164
204
  catch (error) {
165
205
  if (signal?.aborted || isAbortError(error)) {
package/dist/runtime.js CHANGED
@@ -130,9 +130,15 @@ export function createRuntimeSdk(options) {
130
130
  decision: mostRestrictiveHookDecision(postRecords),
131
131
  records: postRecords,
132
132
  });
133
- const result = options.host.appendHookEvidence
133
+ let result = options.host.appendHookEvidence
134
134
  ? options.host.appendHookEvidence(execution.result, [...preRecords, ...postRecords], preDecision)
135
135
  : execution.result;
136
+ // Host-only sideband must survive a Hook Evidence clone without becoming
137
+ // enumerable event/storage data. Model-supplied fields cannot create it.
138
+ const sideband = Object.getOwnPropertyDescriptor(execution.result, 'modelContext');
139
+ if (result !== execution.result && sideband && sideband.enumerable === false) {
140
+ result = Object.defineProperty({ ...result }, 'modelContext', sideband);
141
+ }
136
142
  const resolvedExecution = { ...execution, result };
137
143
  emit({ type: 'tool.completed', ...eventBase, decision: preDecision, result });
138
144
  return resolvedExecution;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peer-agent/runtime-sdk",
3
- "version": "0.1.0-rc.3",
3
+ "version": "0.1.0-rc.31",
4
4
  "description": "Public host-neutral orchestration SDK for Peer Agent runtimes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,8 +34,8 @@
34
34
  "LICENSE"
35
35
  ],
36
36
  "dependencies": {
37
- "@peer-agent/protocol": "^0.1.0-rc.3",
38
- "@peer-agent/runtime-core": "^0.1.0-rc.3"
37
+ "@peer-agent/protocol": "^0.1.0-rc.31",
38
+ "@peer-agent/runtime-core": "^0.1.0-rc.31"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^5.9.3"