@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.
Files changed (49) hide show
  1. package/.claude/settings.local.json +27 -0
  2. package/AGENTS.md +681 -0
  3. package/CLAUDE.md +681 -0
  4. package/README.md +15 -0
  5. package/bun.lock +472 -0
  6. package/eslint.config.js +75 -0
  7. package/index.ts +1 -0
  8. package/llms.md +796 -0
  9. package/package.json +37 -0
  10. package/specs/UAP-1.0.md +2355 -0
  11. package/src/agent/index.ts +384 -0
  12. package/src/agent/types.ts +91 -0
  13. package/src/checkpoint/file.ts +126 -0
  14. package/src/checkpoint/index.ts +40 -0
  15. package/src/checkpoint/types.ts +95 -0
  16. package/src/execution/index.ts +37 -0
  17. package/src/execution/loop.ts +310 -0
  18. package/src/execution/plan.ts +497 -0
  19. package/src/execution/react.ts +340 -0
  20. package/src/execution/tool-ordering.ts +186 -0
  21. package/src/execution/types.ts +315 -0
  22. package/src/index.ts +80 -0
  23. package/src/middleware/index.ts +7 -0
  24. package/src/middleware/logging.ts +123 -0
  25. package/src/middleware/types.ts +69 -0
  26. package/src/state/index.ts +301 -0
  27. package/src/state/types.ts +173 -0
  28. package/src/thread-tree/index.ts +249 -0
  29. package/src/thread-tree/types.ts +29 -0
  30. package/src/utils/uuid.ts +7 -0
  31. package/tests/live/agent-anthropic.test.ts +288 -0
  32. package/tests/live/agent-strategy-hooks.test.ts +268 -0
  33. package/tests/live/checkpoint.test.ts +243 -0
  34. package/tests/live/execution-strategies.test.ts +255 -0
  35. package/tests/live/plan-strategy.test.ts +160 -0
  36. package/tests/live/subagent-events.live.test.ts +249 -0
  37. package/tests/live/thread-tree.test.ts +186 -0
  38. package/tests/unit/agent.test.ts +703 -0
  39. package/tests/unit/checkpoint.test.ts +232 -0
  40. package/tests/unit/execution/equivalence.test.ts +402 -0
  41. package/tests/unit/execution/loop.test.ts +437 -0
  42. package/tests/unit/execution/plan.test.ts +590 -0
  43. package/tests/unit/execution/react.test.ts +604 -0
  44. package/tests/unit/execution/subagent-events.test.ts +235 -0
  45. package/tests/unit/execution/tool-ordering.test.ts +310 -0
  46. package/tests/unit/middleware/logging.test.ts +276 -0
  47. package/tests/unit/state.test.ts +573 -0
  48. package/tests/unit/thread-tree.test.ts +249 -0
  49. 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
+ }