@providerprotocol/agents 0.0.1
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/.claude/settings.local.json +27 -0
- package/AGENTS.md +681 -0
- package/CLAUDE.md +681 -0
- package/README.md +15 -0
- package/bun.lock +472 -0
- package/eslint.config.js +75 -0
- package/index.ts +1 -0
- package/llms.md +796 -0
- package/package.json +37 -0
- package/specs/UAP-1.0.md +2355 -0
- package/src/agent/index.ts +384 -0
- package/src/agent/types.ts +91 -0
- package/src/checkpoint/file.ts +126 -0
- package/src/checkpoint/index.ts +40 -0
- package/src/checkpoint/types.ts +95 -0
- package/src/execution/index.ts +37 -0
- package/src/execution/loop.ts +310 -0
- package/src/execution/plan.ts +497 -0
- package/src/execution/react.ts +340 -0
- package/src/execution/tool-ordering.ts +186 -0
- package/src/execution/types.ts +315 -0
- package/src/index.ts +80 -0
- package/src/middleware/index.ts +7 -0
- package/src/middleware/logging.ts +123 -0
- package/src/middleware/types.ts +69 -0
- package/src/state/index.ts +301 -0
- package/src/state/types.ts +173 -0
- package/src/thread-tree/index.ts +249 -0
- package/src/thread-tree/types.ts +29 -0
- package/src/utils/uuid.ts +7 -0
- package/tests/live/agent-anthropic.test.ts +288 -0
- package/tests/live/agent-strategy-hooks.test.ts +268 -0
- package/tests/live/checkpoint.test.ts +243 -0
- package/tests/live/execution-strategies.test.ts +255 -0
- package/tests/live/plan-strategy.test.ts +160 -0
- package/tests/live/subagent-events.live.test.ts +249 -0
- package/tests/live/thread-tree.test.ts +186 -0
- package/tests/unit/agent.test.ts +703 -0
- package/tests/unit/checkpoint.test.ts +232 -0
- package/tests/unit/execution/equivalence.test.ts +402 -0
- package/tests/unit/execution/loop.test.ts +437 -0
- package/tests/unit/execution/plan.test.ts +590 -0
- package/tests/unit/execution/react.test.ts +604 -0
- package/tests/unit/execution/subagent-events.test.ts +235 -0
- package/tests/unit/execution/tool-ordering.test.ts +310 -0
- package/tests/unit/middleware/logging.test.ts +276 -0
- package/tests/unit/state.test.ts +573 -0
- package/tests/unit/thread-tree.test.ts +249 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for step-level persistence and session resume.
|
|
5
|
+
*
|
|
6
|
+
* @see UAP-1.0 Spec Section 12.4
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentStateJSON } from '../state/types.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checkpoint store interface.
|
|
13
|
+
*
|
|
14
|
+
* Implementations handle persistence of agent state at step boundaries
|
|
15
|
+
* for crash recovery and session resume.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const store = fileCheckpoints({ dir: './checkpoints' });
|
|
20
|
+
*
|
|
21
|
+
* // Save checkpoint
|
|
22
|
+
* await store.save('session-123', state.toJSON());
|
|
23
|
+
*
|
|
24
|
+
* // Load checkpoint
|
|
25
|
+
* const saved = await store.load('session-123');
|
|
26
|
+
* if (saved) {
|
|
27
|
+
* const restored = AgentState.fromJSON(saved);
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export interface CheckpointStore {
|
|
32
|
+
/**
|
|
33
|
+
* Save a checkpoint at the current state.
|
|
34
|
+
*
|
|
35
|
+
* @param sessionId - Session identifier
|
|
36
|
+
* @param state - Serialized agent state
|
|
37
|
+
*/
|
|
38
|
+
save(sessionId: string, state: AgentStateJSON): Promise<void>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load the most recent checkpoint for a session.
|
|
42
|
+
*
|
|
43
|
+
* @param sessionId - Session identifier
|
|
44
|
+
* @returns Serialized state or null if not found
|
|
45
|
+
*/
|
|
46
|
+
load(sessionId: string): Promise<AgentStateJSON | null>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Delete all checkpoints for a session.
|
|
50
|
+
*
|
|
51
|
+
* @param sessionId - Session identifier
|
|
52
|
+
*/
|
|
53
|
+
delete(sessionId: string): Promise<void>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* List all session IDs with checkpoints.
|
|
57
|
+
*
|
|
58
|
+
* @returns Array of session IDs
|
|
59
|
+
*/
|
|
60
|
+
list(): Promise<string[]>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Options for file-based checkpoint store.
|
|
65
|
+
*/
|
|
66
|
+
export interface FileCheckpointOptions {
|
|
67
|
+
/** Directory for checkpoint files. Default: ".checkpoints" */
|
|
68
|
+
dir?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checkpoint metadata stored alongside state.
|
|
73
|
+
*/
|
|
74
|
+
export interface CheckpointMetadata {
|
|
75
|
+
/** Session identifier */
|
|
76
|
+
sessionId: string;
|
|
77
|
+
/** Unique checkpoint ID */
|
|
78
|
+
checkpointId: string;
|
|
79
|
+
/** ISO 8601 timestamp */
|
|
80
|
+
timestamp: string;
|
|
81
|
+
/** Step number at checkpoint */
|
|
82
|
+
step: number;
|
|
83
|
+
/** Agent instance ID */
|
|
84
|
+
agentId: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Full checkpoint data including state and metadata.
|
|
89
|
+
*/
|
|
90
|
+
export interface CheckpointData {
|
|
91
|
+
/** Checkpoint metadata */
|
|
92
|
+
metadata: CheckpointMetadata;
|
|
93
|
+
/** Serialized agent state */
|
|
94
|
+
state: AgentStateJSON;
|
|
95
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { loop } from './loop.ts';
|
|
2
|
+
export { react } from './react.ts';
|
|
3
|
+
export { plan } from './plan.ts';
|
|
4
|
+
|
|
5
|
+
// Tool ordering utilities
|
|
6
|
+
export {
|
|
7
|
+
orderToolCalls,
|
|
8
|
+
hasToolDependencies,
|
|
9
|
+
hasCallDependencies,
|
|
10
|
+
} from './tool-ordering.ts';
|
|
11
|
+
export type { ExecutionGroup } from './tool-ordering.ts';
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
ExecutionStrategy,
|
|
15
|
+
ExecutionContext,
|
|
16
|
+
ExecutionResult,
|
|
17
|
+
LoopOptions,
|
|
18
|
+
ReactOptions,
|
|
19
|
+
PlanOptions,
|
|
20
|
+
AgentStrategy,
|
|
21
|
+
GenerateResult,
|
|
22
|
+
AgentStreamResult,
|
|
23
|
+
AgentStreamEvent,
|
|
24
|
+
UAPEventType,
|
|
25
|
+
// Tool dependency types (Section 8.5)
|
|
26
|
+
ToolDependencyOptions,
|
|
27
|
+
ToolWithDependencies,
|
|
28
|
+
OrderedToolCall,
|
|
29
|
+
// Sub-agent event types (Section 8.7)
|
|
30
|
+
SubagentEventType,
|
|
31
|
+
SubagentEventBase,
|
|
32
|
+
SubagentStartEvent,
|
|
33
|
+
SubagentInnerEvent,
|
|
34
|
+
SubagentEndEvent,
|
|
35
|
+
SubagentEvent,
|
|
36
|
+
OnSubagentEvent,
|
|
37
|
+
} from './types.ts';
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { Turn, StreamEvent } from '@providerprotocol/ai';
|
|
2
|
+
import type {
|
|
3
|
+
ExecutionStrategy,
|
|
4
|
+
ExecutionContext,
|
|
5
|
+
ExecutionResult,
|
|
6
|
+
LoopOptions,
|
|
7
|
+
AgentStreamResult,
|
|
8
|
+
AgentStreamEvent,
|
|
9
|
+
} from './types.ts';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_LOOP_OPTIONS: Required<LoopOptions> = {
|
|
12
|
+
maxIterations: Infinity,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a loop execution strategy.
|
|
17
|
+
* The simplest strategy - equivalent to UPP's tool loop behavior.
|
|
18
|
+
*
|
|
19
|
+
* Behavior:
|
|
20
|
+
* 1. Send input to LLM
|
|
21
|
+
* 2. If response has tool calls, execute tools and loop
|
|
22
|
+
* 3. Continue until no tool calls or maxIterations reached
|
|
23
|
+
* 4. Return final response as UPP Turn
|
|
24
|
+
*
|
|
25
|
+
* @param options - Loop configuration options
|
|
26
|
+
* @returns ExecutionStrategy
|
|
27
|
+
*/
|
|
28
|
+
export function loop(options: LoopOptions = {}): ExecutionStrategy {
|
|
29
|
+
const opts = { ...DEFAULT_LOOP_OPTIONS, ...options };
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name: 'loop',
|
|
33
|
+
|
|
34
|
+
async execute(context: ExecutionContext): Promise<ExecutionResult> {
|
|
35
|
+
const { llm, input, state, strategy, signal } = context;
|
|
36
|
+
|
|
37
|
+
// Add input message to state and set agentId in metadata
|
|
38
|
+
// This ensures checkpoints include the full conversation
|
|
39
|
+
let currentState = state
|
|
40
|
+
.withMessage(input)
|
|
41
|
+
.withMetadata('agentId', context.agent.id);
|
|
42
|
+
let iteration = 0;
|
|
43
|
+
let finalTurn: Turn | undefined;
|
|
44
|
+
|
|
45
|
+
// Messages for LLM generation (includes input we just added)
|
|
46
|
+
const inputMessages = [...currentState.messages];
|
|
47
|
+
|
|
48
|
+
while (true) {
|
|
49
|
+
iteration++;
|
|
50
|
+
currentState = currentState.withStep(iteration);
|
|
51
|
+
|
|
52
|
+
// Check abort signal
|
|
53
|
+
if (signal?.aborted) {
|
|
54
|
+
throw new Error('Execution aborted');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Call strategy hooks
|
|
58
|
+
strategy.onStepStart?.(iteration, currentState);
|
|
59
|
+
|
|
60
|
+
// Generate response - llm.generate uses rest params, pass messages array
|
|
61
|
+
const turn = await llm.generate(inputMessages);
|
|
62
|
+
finalTurn = turn;
|
|
63
|
+
|
|
64
|
+
// Update state with messages from this turn
|
|
65
|
+
currentState = currentState.withMessages(turn.messages);
|
|
66
|
+
|
|
67
|
+
// Call action hook if there were tool calls
|
|
68
|
+
if (turn.response.hasToolCalls) {
|
|
69
|
+
strategy.onAct?.(iteration, turn.response.toolCalls ?? []);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Call observe hook if there were tool executions
|
|
73
|
+
if (turn.toolExecutions && turn.toolExecutions.length > 0) {
|
|
74
|
+
strategy.onObserve?.(iteration, turn.toolExecutions);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Call step end hook
|
|
78
|
+
strategy.onStepEnd?.(iteration, { turn, state: currentState });
|
|
79
|
+
|
|
80
|
+
// Save checkpoint after step completes (fire-and-forget, log errors)
|
|
81
|
+
if (context.checkpoints && context.sessionId) {
|
|
82
|
+
context.checkpoints.save(context.sessionId, currentState.toJSON()).catch((err) => {
|
|
83
|
+
console.error('[UAP] Checkpoint save failed:', err);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check stop condition
|
|
88
|
+
const shouldStop = await strategy.stopCondition?.(currentState);
|
|
89
|
+
if (shouldStop) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if there are more tool calls to process
|
|
94
|
+
// UPP's llm.generate handles the tool loop internally,
|
|
95
|
+
// so we only need one iteration unless we're doing multi-step
|
|
96
|
+
if (!turn.response.hasToolCalls) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check iteration limit
|
|
101
|
+
if (opts.maxIterations !== Infinity && iteration >= opts.maxIterations) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// For next iteration, use the updated messages
|
|
106
|
+
inputMessages.length = 0;
|
|
107
|
+
inputMessages.push(...currentState.messages);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!finalTurn) {
|
|
111
|
+
throw new Error('No turn generated');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Include sessionId in state metadata if checkpointing is enabled
|
|
115
|
+
// Per UAP spec Section 3.4: sessionId MUST be included in state.metadata
|
|
116
|
+
let finalState = currentState;
|
|
117
|
+
if (context.sessionId) {
|
|
118
|
+
finalState = currentState.withMetadata('sessionId', context.sessionId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result: ExecutionResult = {
|
|
122
|
+
turn: finalTurn,
|
|
123
|
+
state: finalState,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
strategy.onComplete?.(result);
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
stream(context: ExecutionContext): AgentStreamResult {
|
|
132
|
+
const { llm, input, state, strategy, signal } = context;
|
|
133
|
+
const agentId = context.agent.id;
|
|
134
|
+
|
|
135
|
+
let aborted = false;
|
|
136
|
+
const abortController = new AbortController();
|
|
137
|
+
|
|
138
|
+
// Combine signals if one was provided
|
|
139
|
+
if (signal) {
|
|
140
|
+
signal.addEventListener('abort', () => abortController.abort());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let resolveResult: (result: ExecutionResult) => void;
|
|
144
|
+
let rejectResult: (error: Error) => void;
|
|
145
|
+
|
|
146
|
+
const resultPromise = new Promise<ExecutionResult>((resolve, reject) => {
|
|
147
|
+
resolveResult = resolve;
|
|
148
|
+
rejectResult = reject;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
async function* generateEvents(): AsyncGenerator<AgentStreamEvent> {
|
|
152
|
+
// Add input message to state and set agentId in metadata
|
|
153
|
+
// This ensures checkpoints include the full conversation
|
|
154
|
+
let currentState = state
|
|
155
|
+
.withMessage(input)
|
|
156
|
+
.withMetadata('agentId', context.agent.id);
|
|
157
|
+
let iteration = 0;
|
|
158
|
+
let finalTurn: Turn | undefined;
|
|
159
|
+
|
|
160
|
+
// Messages for LLM generation (includes input we just added)
|
|
161
|
+
const inputMessages = [...currentState.messages];
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
while (!aborted) {
|
|
165
|
+
iteration++;
|
|
166
|
+
currentState = currentState.withStep(iteration);
|
|
167
|
+
|
|
168
|
+
if (abortController.signal.aborted) {
|
|
169
|
+
throw new Error('Execution aborted');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
strategy.onStepStart?.(iteration, currentState);
|
|
173
|
+
|
|
174
|
+
// Emit step start event
|
|
175
|
+
yield {
|
|
176
|
+
source: 'uap',
|
|
177
|
+
uap: {
|
|
178
|
+
type: 'step_start',
|
|
179
|
+
step: iteration,
|
|
180
|
+
agentId,
|
|
181
|
+
data: { iteration },
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Stream the LLM response
|
|
186
|
+
const streamResult = llm.stream(inputMessages);
|
|
187
|
+
|
|
188
|
+
for await (const event of streamResult as AsyncIterable<StreamEvent>) {
|
|
189
|
+
if (abortController.signal.aborted) {
|
|
190
|
+
throw new Error('Execution aborted');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Yield UPP events
|
|
194
|
+
yield {
|
|
195
|
+
source: 'upp',
|
|
196
|
+
upp: event,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Get the final turn from the stream
|
|
201
|
+
const turn = await streamResult.turn;
|
|
202
|
+
finalTurn = turn;
|
|
203
|
+
|
|
204
|
+
currentState = currentState.withMessages(turn.messages);
|
|
205
|
+
|
|
206
|
+
if (turn.response.hasToolCalls) {
|
|
207
|
+
strategy.onAct?.(iteration, turn.response.toolCalls ?? []);
|
|
208
|
+
|
|
209
|
+
yield {
|
|
210
|
+
source: 'uap',
|
|
211
|
+
uap: {
|
|
212
|
+
type: 'action',
|
|
213
|
+
step: iteration,
|
|
214
|
+
agentId,
|
|
215
|
+
data: { toolCalls: turn.response.toolCalls },
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (turn.toolExecutions && turn.toolExecutions.length > 0) {
|
|
221
|
+
strategy.onObserve?.(iteration, turn.toolExecutions);
|
|
222
|
+
|
|
223
|
+
yield {
|
|
224
|
+
source: 'uap',
|
|
225
|
+
uap: {
|
|
226
|
+
type: 'observation',
|
|
227
|
+
step: iteration,
|
|
228
|
+
agentId,
|
|
229
|
+
data: { observations: turn.toolExecutions },
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
strategy.onStepEnd?.(iteration, { turn, state: currentState });
|
|
235
|
+
|
|
236
|
+
// Save checkpoint after step completes (fire-and-forget, log errors)
|
|
237
|
+
if (context.checkpoints && context.sessionId) {
|
|
238
|
+
context.checkpoints.save(context.sessionId, currentState.toJSON()).catch((err) => {
|
|
239
|
+
console.error('[UAP] Checkpoint save failed:', err);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
yield {
|
|
244
|
+
source: 'uap',
|
|
245
|
+
uap: {
|
|
246
|
+
type: 'step_end',
|
|
247
|
+
step: iteration,
|
|
248
|
+
agentId,
|
|
249
|
+
data: { iteration },
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const shouldStop = await strategy.stopCondition?.(currentState);
|
|
254
|
+
if (shouldStop) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!turn.response.hasToolCalls) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (opts.maxIterations !== Infinity && iteration >= opts.maxIterations) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
inputMessages.length = 0;
|
|
267
|
+
inputMessages.push(...currentState.messages);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!finalTurn) {
|
|
271
|
+
throw new Error('No turn generated');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Include sessionId in state metadata if checkpointing is enabled
|
|
275
|
+
// Per UAP spec Section 3.4: sessionId MUST be included in state.metadata
|
|
276
|
+
let finalState = currentState;
|
|
277
|
+
if (context.sessionId) {
|
|
278
|
+
finalState = currentState.withMetadata('sessionId', context.sessionId);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const result: ExecutionResult = {
|
|
282
|
+
turn: finalTurn,
|
|
283
|
+
state: finalState,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
strategy.onComplete?.(result);
|
|
287
|
+
resolveResult(result);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
290
|
+
strategy.onError?.(err, currentState);
|
|
291
|
+
rejectResult(err);
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const iterator = generateEvents();
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
[Symbol.asyncIterator]() {
|
|
300
|
+
return iterator;
|
|
301
|
+
},
|
|
302
|
+
result: resultPromise,
|
|
303
|
+
abort() {
|
|
304
|
+
aborted = true;
|
|
305
|
+
abortController.abort();
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|