@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,497 @@
1
+ import type { Turn, StreamEvent } from '@providerprotocol/ai';
2
+ import { UserMessage } from '@providerprotocol/ai';
3
+ import type { PlanStep } from '../state/index.ts';
4
+ import { generateUUID } from '../utils/uuid.ts';
5
+ import type {
6
+ ExecutionStrategy,
7
+ ExecutionContext,
8
+ ExecutionResult,
9
+ PlanOptions,
10
+ AgentStreamResult,
11
+ AgentStreamEvent,
12
+ } from './types.ts';
13
+
14
+ const DEFAULT_PLAN_OPTIONS: Required<PlanOptions> = {
15
+ maxPlanSteps: Infinity,
16
+ allowReplan: true,
17
+ planSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ steps: {
21
+ type: 'array',
22
+ items: {
23
+ type: 'object',
24
+ properties: {
25
+ id: { type: 'string', description: 'Unique step identifier' },
26
+ description: { type: 'string', description: 'What this step does' },
27
+ tool: { type: 'string', description: 'Tool to use (if applicable)' },
28
+ dependsOn: {
29
+ type: 'array',
30
+ items: { type: 'string' },
31
+ description: 'IDs of steps this depends on',
32
+ },
33
+ },
34
+ required: ['id', 'description', 'dependsOn'],
35
+ },
36
+ },
37
+ },
38
+ required: ['steps'],
39
+ },
40
+ };
41
+
42
+ const PLAN_PROMPT = `Create a detailed execution plan to accomplish the task.
43
+ Break it down into clear steps, specifying which tool to use for each step if applicable.
44
+ Include dependencies between steps (which steps must complete before others can start).
45
+ Return your plan as a JSON object with a "steps" array.`;
46
+
47
+ /**
48
+ * Create a plan-then-execute strategy.
49
+ *
50
+ * Behavior:
51
+ * 1. Plan: LLM generates structured plan with steps and dependencies
52
+ * 2. Execute: Execute each plan step respecting dependency order
53
+ * 3. Replan: If a step fails and allowReplan is true, generate new plan
54
+ *
55
+ * @param options - Plan configuration options
56
+ * @returns ExecutionStrategy
57
+ */
58
+ export function plan(options: PlanOptions = {}): ExecutionStrategy {
59
+ const opts = { ...DEFAULT_PLAN_OPTIONS, ...options };
60
+
61
+ return {
62
+ name: 'plan',
63
+
64
+ async execute(context: ExecutionContext): Promise<ExecutionResult> {
65
+ const { llm, input, state, strategy, signal } = context;
66
+
67
+ // Add input message to state and set agentId in metadata
68
+ // This ensures checkpoints include the full conversation
69
+ let currentState = state
70
+ .withMessage(input)
71
+ .withMetadata('agentId', context.agent.id);
72
+ let step = 0;
73
+ let finalTurn: Turn | undefined;
74
+
75
+ // Messages for LLM generation (includes input we just added)
76
+ const messages = [...currentState.messages];
77
+
78
+ // PLANNING PHASE
79
+ step++;
80
+ currentState = currentState.withStep(step);
81
+
82
+ if (signal?.aborted) {
83
+ throw new Error('Execution aborted');
84
+ }
85
+
86
+ strategy.onStepStart?.(step, currentState);
87
+
88
+ // Generate the plan
89
+ const planMessages = [
90
+ ...messages,
91
+ new UserMessage(PLAN_PROMPT),
92
+ ];
93
+
94
+ const planTurn = await llm.generate(planMessages);
95
+
96
+ // Parse the plan from the response
97
+ let planData: { steps: Array<{ id: string; description: string; tool?: string; dependsOn: string[] }> };
98
+
99
+ try {
100
+ if (planTurn.data) {
101
+ planData = planTurn.data as typeof planData;
102
+ } else {
103
+ // Try to parse from text
104
+ const jsonMatch = planTurn.response.text.match(/\{[\s\S]*\}/);
105
+ if (jsonMatch) {
106
+ planData = JSON.parse(jsonMatch[0]) as typeof planData;
107
+ } else {
108
+ throw new Error('Could not parse plan from response');
109
+ }
110
+ }
111
+ } catch (err) {
112
+ throw new Error(`Failed to parse execution plan: ${err instanceof Error ? err.message : String(err)}`);
113
+ }
114
+
115
+ // Convert to PlanStep format
116
+ let planSteps: PlanStep[] = planData.steps.map((s) => ({
117
+ id: s.id || generateUUID(),
118
+ description: s.description,
119
+ tool: s.tool,
120
+ dependsOn: s.dependsOn || [],
121
+ status: 'pending' as const,
122
+ }));
123
+
124
+ // Apply maxPlanSteps limit
125
+ if (opts.maxPlanSteps !== Infinity && planSteps.length > opts.maxPlanSteps) {
126
+ planSteps = planSteps.slice(0, opts.maxPlanSteps);
127
+ }
128
+
129
+ currentState = currentState.withPlan(planSteps);
130
+ messages.push(...planTurn.messages);
131
+
132
+ strategy.onStepEnd?.(step, { turn: planTurn, state: currentState });
133
+
134
+ // EXECUTION PHASE
135
+ const completedSteps = new Set<string>();
136
+
137
+ // Execute steps in topological order
138
+ while (planSteps.some((s) => s.status === 'pending')) {
139
+ // Find next executable step (all dependencies completed)
140
+ const nextStep = planSteps.find(
141
+ (s) => s.status === 'pending'
142
+ && s.dependsOn.every((depId) => completedSteps.has(depId)),
143
+ );
144
+
145
+ if (!nextStep) {
146
+ // No step can be executed - either done or cyclic dependency
147
+ break;
148
+ }
149
+
150
+ step++;
151
+ currentState = currentState.withStep(step);
152
+
153
+ if (signal?.aborted) {
154
+ throw new Error('Execution aborted');
155
+ }
156
+
157
+ strategy.onStepStart?.(step, currentState);
158
+
159
+ // Update step status to in_progress
160
+ nextStep.status = 'in_progress';
161
+ currentState = currentState.withPlan([...planSteps]);
162
+
163
+ // Execute the step
164
+ const stepPrompt = new UserMessage(
165
+ `Execute step "${nextStep.id}": ${nextStep.description}${nextStep.tool ? ` using the ${nextStep.tool} tool` : ''}`,
166
+ );
167
+ messages.push(stepPrompt);
168
+
169
+ try {
170
+ const stepTurn = await llm.generate(messages);
171
+ finalTurn = stepTurn;
172
+
173
+ messages.push(...stepTurn.messages);
174
+ currentState = currentState.withMessages(stepTurn.messages);
175
+
176
+ if (stepTurn.response.hasToolCalls) {
177
+ strategy.onAct?.(step, stepTurn.response.toolCalls ?? []);
178
+ }
179
+
180
+ if (stepTurn.toolExecutions && stepTurn.toolExecutions.length > 0) {
181
+ strategy.onObserve?.(step, stepTurn.toolExecutions);
182
+ }
183
+
184
+ // Mark step as completed
185
+ nextStep.status = 'completed';
186
+ completedSteps.add(nextStep.id);
187
+ currentState = currentState.withPlan([...planSteps]);
188
+
189
+ strategy.onStepEnd?.(step, { turn: stepTurn, state: currentState });
190
+ } catch (err) {
191
+ nextStep.status = 'failed';
192
+ currentState = currentState.withPlan([...planSteps]);
193
+
194
+ if (opts.allowReplan) {
195
+ // Could implement replanning here
196
+ // For now, just continue and let the error propagate
197
+ }
198
+
199
+ throw err;
200
+ }
201
+
202
+ // Check stop condition
203
+ const shouldStop = await strategy.stopCondition?.(currentState);
204
+ if (shouldStop) {
205
+ break;
206
+ }
207
+ }
208
+
209
+ if (!finalTurn) {
210
+ finalTurn = planTurn; // Use plan turn if no execution happened
211
+ }
212
+
213
+ // Include sessionId in state metadata if checkpointing is enabled
214
+ let finalState = currentState;
215
+ if (context.sessionId) {
216
+ finalState = currentState.withMetadata('sessionId', context.sessionId);
217
+ }
218
+
219
+ const result: ExecutionResult = {
220
+ turn: finalTurn,
221
+ state: finalState,
222
+ };
223
+
224
+ strategy.onComplete?.(result);
225
+
226
+ return result;
227
+ },
228
+
229
+ stream(context: ExecutionContext): AgentStreamResult {
230
+ const { llm, input, state, strategy, signal } = context;
231
+ const agentId = context.agent.id;
232
+
233
+ let aborted = false;
234
+ const abortController = new AbortController();
235
+
236
+ if (signal) {
237
+ signal.addEventListener('abort', () => abortController.abort());
238
+ }
239
+
240
+ let resolveResult: (result: ExecutionResult) => void;
241
+ let rejectResult: (error: Error) => void;
242
+
243
+ const resultPromise = new Promise<ExecutionResult>((resolve, reject) => {
244
+ resolveResult = resolve;
245
+ rejectResult = reject;
246
+ });
247
+
248
+ async function* generateEvents(): AsyncGenerator<AgentStreamEvent> {
249
+ // Add input message to state and set agentId in metadata
250
+ // This ensures checkpoints include the full conversation
251
+ let currentState = state
252
+ .withMessage(input)
253
+ .withMetadata('agentId', context.agent.id);
254
+ let step = 0;
255
+ let finalTurn: Turn | undefined;
256
+
257
+ // Messages for LLM generation (includes input we just added)
258
+ const messages = [...currentState.messages];
259
+
260
+ try {
261
+ // PLANNING PHASE
262
+ step++;
263
+ currentState = currentState.withStep(step);
264
+
265
+ if (abortController.signal.aborted) {
266
+ throw new Error('Execution aborted');
267
+ }
268
+
269
+ strategy.onStepStart?.(step, currentState);
270
+
271
+ yield {
272
+ source: 'uap',
273
+ uap: {
274
+ type: 'step_start',
275
+ step,
276
+ agentId,
277
+ data: { phase: 'planning' },
278
+ },
279
+ };
280
+
281
+ const planMessages = [
282
+ ...messages,
283
+ new UserMessage(PLAN_PROMPT),
284
+ ];
285
+
286
+ const planStream = llm.stream(planMessages);
287
+
288
+ for await (const event of planStream as AsyncIterable<StreamEvent>) {
289
+ if (abortController.signal.aborted) {
290
+ throw new Error('Execution aborted');
291
+ }
292
+
293
+ yield { source: 'upp', upp: event };
294
+ }
295
+
296
+ const planTurn = await planStream.turn;
297
+
298
+ let planData: { steps: Array<{ id: string; description: string; tool?: string; dependsOn: string[] }> };
299
+
300
+ try {
301
+ if (planTurn.data) {
302
+ planData = planTurn.data as typeof planData;
303
+ } else {
304
+ const jsonMatch = planTurn.response.text.match(/\{[\s\S]*\}/);
305
+ if (jsonMatch) {
306
+ planData = JSON.parse(jsonMatch[0]) as typeof planData;
307
+ } else {
308
+ throw new Error('Could not parse plan from response');
309
+ }
310
+ }
311
+ } catch (err) {
312
+ throw new Error(`Failed to parse execution plan: ${err instanceof Error ? err.message : String(err)}`);
313
+ }
314
+
315
+ let planSteps: PlanStep[] = planData.steps.map((s) => ({
316
+ id: s.id || generateUUID(),
317
+ description: s.description,
318
+ tool: s.tool,
319
+ dependsOn: s.dependsOn || [],
320
+ status: 'pending' as const,
321
+ }));
322
+
323
+ if (opts.maxPlanSteps !== Infinity && planSteps.length > opts.maxPlanSteps) {
324
+ planSteps = planSteps.slice(0, opts.maxPlanSteps);
325
+ }
326
+
327
+ currentState = currentState.withPlan(planSteps);
328
+ messages.push(...planTurn.messages);
329
+
330
+ yield {
331
+ source: 'uap',
332
+ uap: {
333
+ type: 'plan_created',
334
+ step,
335
+ agentId,
336
+ data: { plan: planSteps },
337
+ },
338
+ };
339
+
340
+ strategy.onStepEnd?.(step, { turn: planTurn, state: currentState });
341
+
342
+ yield {
343
+ source: 'uap',
344
+ uap: {
345
+ type: 'step_end',
346
+ step,
347
+ agentId,
348
+ data: { phase: 'planning' },
349
+ },
350
+ };
351
+
352
+ // EXECUTION PHASE
353
+ const completedSteps = new Set<string>();
354
+
355
+ while (planSteps.some((s) => s.status === 'pending') && !aborted) {
356
+ const nextStep = planSteps.find(
357
+ (s) => s.status === 'pending'
358
+ && s.dependsOn.every((depId) => completedSteps.has(depId)),
359
+ );
360
+
361
+ if (!nextStep) {
362
+ break;
363
+ }
364
+
365
+ step++;
366
+ currentState = currentState.withStep(step);
367
+
368
+ if (abortController.signal.aborted) {
369
+ throw new Error('Execution aborted');
370
+ }
371
+
372
+ strategy.onStepStart?.(step, currentState);
373
+
374
+ yield {
375
+ source: 'uap',
376
+ uap: {
377
+ type: 'plan_step_start',
378
+ step,
379
+ agentId,
380
+ data: { planStep: nextStep },
381
+ },
382
+ };
383
+
384
+ nextStep.status = 'in_progress';
385
+ currentState = currentState.withPlan([...planSteps]);
386
+
387
+ const stepPrompt = new UserMessage(
388
+ `Execute step "${nextStep.id}": ${nextStep.description}${nextStep.tool ? ` using the ${nextStep.tool} tool` : ''}`,
389
+ );
390
+ messages.push(stepPrompt);
391
+
392
+ const stepStream = llm.stream(messages);
393
+
394
+ for await (const event of stepStream as AsyncIterable<StreamEvent>) {
395
+ if (abortController.signal.aborted) {
396
+ throw new Error('Execution aborted');
397
+ }
398
+
399
+ yield { source: 'upp', upp: event };
400
+ }
401
+
402
+ const stepTurn = await stepStream.turn;
403
+ finalTurn = stepTurn;
404
+
405
+ messages.push(...stepTurn.messages);
406
+ currentState = currentState.withMessages(stepTurn.messages);
407
+
408
+ if (stepTurn.response.hasToolCalls) {
409
+ strategy.onAct?.(step, stepTurn.response.toolCalls ?? []);
410
+
411
+ yield {
412
+ source: 'uap',
413
+ uap: {
414
+ type: 'action',
415
+ step,
416
+ agentId,
417
+ data: { toolCalls: stepTurn.response.toolCalls },
418
+ },
419
+ };
420
+ }
421
+
422
+ if (stepTurn.toolExecutions && stepTurn.toolExecutions.length > 0) {
423
+ strategy.onObserve?.(step, stepTurn.toolExecutions);
424
+
425
+ yield {
426
+ source: 'uap',
427
+ uap: {
428
+ type: 'observation',
429
+ step,
430
+ agentId,
431
+ data: { observations: stepTurn.toolExecutions },
432
+ },
433
+ };
434
+ }
435
+
436
+ nextStep.status = 'completed';
437
+ completedSteps.add(nextStep.id);
438
+ currentState = currentState.withPlan([...planSteps]);
439
+
440
+ strategy.onStepEnd?.(step, { turn: stepTurn, state: currentState });
441
+
442
+ yield {
443
+ source: 'uap',
444
+ uap: {
445
+ type: 'plan_step_end',
446
+ step,
447
+ agentId,
448
+ data: { planStep: nextStep },
449
+ },
450
+ };
451
+
452
+ const shouldStop = await strategy.stopCondition?.(currentState);
453
+ if (shouldStop) {
454
+ break;
455
+ }
456
+ }
457
+
458
+ if (!finalTurn) {
459
+ finalTurn = planTurn;
460
+ }
461
+
462
+ // Include sessionId in state metadata if checkpointing is enabled
463
+ let finalState = currentState;
464
+ if (context.sessionId) {
465
+ finalState = currentState.withMetadata('sessionId', context.sessionId);
466
+ }
467
+
468
+ const result: ExecutionResult = {
469
+ turn: finalTurn,
470
+ state: finalState,
471
+ };
472
+
473
+ strategy.onComplete?.(result);
474
+ resolveResult(result);
475
+ } catch (error) {
476
+ const err = error instanceof Error ? error : new Error(String(error));
477
+ strategy.onError?.(err, currentState);
478
+ rejectResult(err);
479
+ throw err;
480
+ }
481
+ }
482
+
483
+ const iterator = generateEvents();
484
+
485
+ return {
486
+ [Symbol.asyncIterator]() {
487
+ return iterator;
488
+ },
489
+ result: resultPromise,
490
+ abort() {
491
+ aborted = true;
492
+ abortController.abort();
493
+ },
494
+ };
495
+ },
496
+ };
497
+ }