@peer-agent/runtime-sdk 0.1.0-rc.7 → 0.1.0-rc.9

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.
@@ -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,11 @@ 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
+ readonly maxToolBatchCalls?: number;
12
17
  }
13
18
  export interface RuntimePipelineToolCall<TPayload = unknown> {
14
19
  readonly toolCallId: string;
@@ -52,9 +57,12 @@ export interface RuntimePipelineModelAdapter<TInput = unknown, TState = unknown,
52
57
  onCompleted?(state: TState, output: TOutput | undefined, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
53
58
  onStopped?(state: TState, executions: readonly RuntimePipelineToolExecution<TCall, TToolResult>[], context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
54
59
  onCancelled?(state: TState | undefined, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
55
- onExhausted?(state: TState, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
60
+ onYield?(state: TState, context: RuntimePipelineTurnContext<TInput>): void | Promise<void>;
61
+ onExhausted?(state: TState, context: RuntimePipelineTurnContext<TInput>, reason: 'max_turns_exceeded' | 'max_tool_calls_exceeded'): void | Promise<void>;
56
62
  }
57
63
  export interface RuntimePipelineToolExecutor<TInput = unknown, TCall extends RuntimePipelineToolCall = RuntimePipelineToolCall, TToolResult = RuntimeSdkToolResult> {
64
+ /** Build a paired, explicit non-execution result without dispatching a capability. */
65
+ notExecuted?(call: TCall, reason: string): RuntimePipelineToolExecution<TCall, TToolResult> | Promise<RuntimePipelineToolExecution<TCall, TToolResult>>;
58
66
  execute(call: TCall, context: RuntimePipelineTurnContext<TInput> & {
59
67
  readonly index: number;
60
68
  }): 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({
@@ -93,30 +106,62 @@ export function createRuntimePipeline(options) {
93
106
  ...(outcome.reason ? { reason: outcome.reason } : {}),
94
107
  };
95
108
  }
109
+ // Reserve the whole batch before dispatch. Never execute a partial
110
+ // write batch or exceed a caller's remaining budget inside one turn.
111
+ if (outcome.calls.length > maxBatch)
112
+ throw new Error('tool_batch_too_large');
113
+ if (toolCalls + outcome.calls.length > maxToolCalls) {
114
+ if (options.tools.notExecuted) {
115
+ const denied = await Promise.all(outcome.calls.map(call => options.tools.notExecuted(call, 'max_tool_calls_exceeded')));
116
+ state = await options.model.applyToolResults(state, denied, currentContext);
117
+ }
118
+ return await exhaust('max_tool_calls_exceeded');
119
+ }
96
120
  const executions = [];
97
121
  if (isParallelSafeLocalToolBatch(outcome.calls)) {
98
122
  const batch = await Promise.all(outcome.calls.map(async (call, index) => {
123
+ if (signal?.aborted && options.tools.notExecuted)
124
+ return options.tools.notExecuted(call, 'cancelled');
99
125
  throwIfAborted(signal);
100
- return options.tools.execute(call, {
101
- ...currentContext,
102
- index,
103
- });
126
+ try {
127
+ return await options.tools.execute(call, { ...currentContext, index });
128
+ }
129
+ catch (error) {
130
+ if (!options.tools.notExecuted)
131
+ throw error;
132
+ return options.tools.notExecuted(call, isAbortError(error) ? 'cancelled_outcome_unknown' : 'execution_outcome_unknown');
133
+ }
104
134
  }));
105
135
  executions.push(...batch);
106
136
  toolCalls += batch.length;
107
137
  }
108
138
  else {
109
139
  for (const [index, call] of outcome.calls.entries()) {
110
- throwIfAborted(signal);
111
- const execution = await options.tools.execute(call, {
112
- ...currentContext,
113
- index,
114
- });
140
+ let execution;
141
+ if (signal?.aborted && options.tools.notExecuted)
142
+ execution = await options.tools.notExecuted(call, 'cancelled');
143
+ else {
144
+ throwIfAborted(signal);
145
+ try {
146
+ execution = await options.tools.execute(call, { ...currentContext, index });
147
+ }
148
+ catch (error) {
149
+ if (!options.tools.notExecuted)
150
+ throw error;
151
+ execution = await options.tools.notExecuted(call, isAbortError(error) ? 'cancelled_outcome_unknown' : 'execution_outcome_unknown');
152
+ }
153
+ }
115
154
  executions.push(execution);
116
155
  toolCalls += 1;
156
+ if (execution.terminal && options.tools.notExecuted) {
157
+ for (const remaining of outcome.calls.slice(index + 1))
158
+ executions.push(await options.tools.notExecuted(remaining, 'batch_stopped'));
159
+ break;
160
+ }
117
161
  }
118
162
  }
119
163
  state = await options.model.applyToolResults(state, executions, currentContext);
164
+ throwIfAborted(signal);
120
165
  try {
121
166
  await options.lifecycle?.toolResultsApplied?.(state, executions, currentContext);
122
167
  }
@@ -143,23 +188,10 @@ export function createRuntimePipeline(options) {
143
188
  reason: terminalExecution.terminalReason || 'tool_requested_stop',
144
189
  };
145
190
  }
191
+ if (toolCalls >= sliceToolCalls)
192
+ return await yieldRun();
146
193
  }
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
- };
194
+ return input.yieldAtTurnLimit ? await yieldRun() : await exhaust('max_turns_exceeded');
163
195
  }
164
196
  catch (error) {
165
197
  if (signal?.aborted || isAbortError(error)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peer-agent/runtime-sdk",
3
- "version": "0.1.0-rc.7",
3
+ "version": "0.1.0-rc.9",
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.7",
38
- "@peer-agent/runtime-core": "^0.1.0-rc.7"
37
+ "@peer-agent/protocol": "^0.1.0-rc.9",
38
+ "@peer-agent/runtime-core": "^0.1.0-rc.9"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^5.9.3"