agency-lang 0.0.90 → 0.0.92

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 (43) hide show
  1. package/dist/lib/backends/agencyGenerator.js +7 -4
  2. package/dist/lib/backends/typescriptBuilder.js +8 -7
  3. package/dist/lib/debugger/driver.js +10 -3
  4. package/dist/lib/debugger/ui.js +1 -0
  5. package/dist/lib/ir/builders.d.ts +6 -2
  6. package/dist/lib/ir/builders.js +4 -4
  7. package/dist/lib/ir/fluent.d.ts +3 -1
  8. package/dist/lib/ir/fluent.js +2 -2
  9. package/dist/lib/ir/prettyPrint.js +4 -2
  10. package/dist/lib/ir/tsIR.d.ts +1 -0
  11. package/dist/lib/parsers/access.test.js +67 -0
  12. package/dist/lib/parsers/binop.test.js +62 -0
  13. package/dist/lib/parsers/parsers.js +21 -12
  14. package/dist/lib/runtime/hooks.d.ts +1 -0
  15. package/dist/lib/runtime/interrupts.js +4 -2
  16. package/dist/lib/runtime/prompt.d.ts +2 -0
  17. package/dist/lib/runtime/prompt.js +17 -8
  18. package/dist/lib/runtime/runner.d.ts +3 -0
  19. package/dist/lib/runtime/runner.js +10 -0
  20. package/dist/lib/runtime/state/checkpointStore.d.ts +1 -0
  21. package/dist/lib/runtime/state/checkpointStore.js +18 -7
  22. package/dist/lib/runtime/state/checkpointStore.test.js +1 -1
  23. package/dist/lib/runtime/state/context.d.ts +4 -0
  24. package/dist/lib/runtime/state/context.js +23 -0
  25. package/dist/lib/types/access.d.ts +3 -0
  26. package/dist/lib/types/binop.d.ts +1 -1
  27. package/dist/lib/types/binop.js +1 -0
  28. package/package.json +2 -2
  29. package/stdlib/agent.js +719 -0
  30. package/stdlib/array.js +1910 -0
  31. package/stdlib/consensus.js +359 -0
  32. package/stdlib/firstValid.js +384 -0
  33. package/stdlib/fs.js +1400 -0
  34. package/stdlib/index.js +1989 -0
  35. package/stdlib/math.js +704 -0
  36. package/stdlib/object.js +945 -1143
  37. package/stdlib/path.js +1107 -0
  38. package/stdlib/retry.js +518 -0
  39. package/stdlib/sample.js +350 -0
  40. package/stdlib/shell.js +1307 -0
  41. package/stdlib/strategy.js +1092 -0
  42. package/stdlib/system.js +850 -0
  43. package/stdlib/weather.js +697 -0
@@ -793,13 +793,16 @@ export class AgencyGenerator {
793
793
  return assigned ? multiLine : this.indentStr(multiLine);
794
794
  }
795
795
  processAccessChainElement(node) {
796
+ const dot = node.optional ? "?." : ".";
796
797
  switch (node.kind) {
797
798
  case "property":
798
- return `.${node.name}`;
799
- case "index":
800
- return `[${this.processNode(node.index).trim()}]`;
799
+ return `${dot}${node.name}`;
800
+ case "index": {
801
+ const inner = this.processNode(node.index).trim();
802
+ return node.optional ? `?.[${inner}]` : `[${inner}]`;
803
+ }
801
804
  case "methodCall":
802
- return `.${this.generateFunctionCallExpression(node.functionCall, "valueAccess")}`;
805
+ return `${dot}${this.generateFunctionCallExpression(node.functionCall, "valueAccess")}`;
803
806
  default:
804
807
  throw new Error(`Unknown access chain element kind: ${node.kind}`);
805
808
  }
@@ -661,10 +661,10 @@ export class TypeScriptBuilder {
661
661
  for (const element of node.chain) {
662
662
  switch (element.kind) {
663
663
  case "property":
664
- result = ts.prop(result, element.name);
664
+ result = ts.prop(result, element.name, { optional: element.optional });
665
665
  break;
666
666
  case "index":
667
- result = ts.index(result, this.processNode(element.index));
667
+ result = ts.index(result, this.processNode(element.index), { optional: element.optional });
668
668
  break;
669
669
  case "methodCall": {
670
670
  const callNode = this.generateFunctionCallExpression(element.functionCall, "valueAccess");
@@ -675,15 +675,14 @@ export class TypeScriptBuilder {
675
675
  const args = isClassMethod
676
676
  ? [...callNode.arguments, this.buildMethodCallConfig()]
677
677
  : callNode.arguments;
678
- const callExpr = $(result)
679
- .prop(callNode.callee.name)
680
- .call(args)
681
- .done();
678
+ const propNode = ts.prop(result, callNode.callee.name, { optional: element.optional });
679
+ const callExpr = ts.call(propNode, args);
682
680
  result = isClassMethod ? ts.await(callExpr) : callExpr;
683
681
  }
684
682
  else {
685
683
  // Fallback for complex cases (e.g. await-wrapped)
686
- result = ts.raw(`${this.str(result)}.${this.str(callNode)}`);
684
+ const dot = element.optional ? "?." : ".";
685
+ result = ts.raw(`${this.str(result)}${dot}${this.str(callNode)}`);
687
686
  }
688
687
  break;
689
688
  }
@@ -1140,6 +1139,7 @@ export class TypeScriptBuilder {
1140
1139
  functionName: ts.str(functionName),
1141
1140
  args: ts.obj(argsObj),
1142
1141
  isBuiltin: ts.bool(false),
1142
+ moduleId: ts.str(this.moduleId),
1143
1143
  }),
1144
1144
  ];
1145
1145
  // Param assignments to stack
@@ -1778,6 +1778,7 @@ export class TypeScriptBuilder {
1778
1778
  runPromptEntries.maxToolCallRounds = ts.num(this.agencyConfig.maxToolCallRounds || 10);
1779
1779
  runPromptEntries.interruptData = ts.raw("__state?.interruptData");
1780
1780
  runPromptEntries.removedTools = ts.self("__removedTools");
1781
+ runPromptEntries.checkpointInfo = ts.raw("runner.getCheckpointInfo()");
1781
1782
  const runPromptCall = $(ts.id("runPrompt"))
1782
1783
  .call([ts.obj(runPromptEntries)])
1783
1784
  .done();
@@ -59,20 +59,22 @@ export class DebuggerDriver {
59
59
  getCallbacks() {
60
60
  return {
61
61
  onFunctionStart: (data) => {
62
- const isInSource = this.isInSourceMap(data.functionName);
62
+ const isAgency = data.moduleId?.endsWith(".agency");
63
63
  this.debuggerState.enterCall();
64
64
  this.ui.state.pushCallStackEntry({
65
65
  functionName: data.functionName,
66
- moduleId: isInSource ? data.functionName : "(ts)",
66
+ moduleId: isAgency ? data.moduleId : "(ts)",
67
67
  line: 0,
68
68
  });
69
- if (!isInSource) {
69
+ if (!isAgency) {
70
70
  this.ui.state.log(`[ts] ${data.functionName}()`);
71
71
  }
72
+ this.ui.render();
72
73
  },
73
74
  onFunctionEnd: (data) => {
74
75
  this.debuggerState.exitCall();
75
76
  this.ui.state.removeWithFuncName(data.functionName);
77
+ this.ui.render();
76
78
  },
77
79
  onNodeStart: (data) => {
78
80
  // gets triggered on every statement, as debugger resumes node execution.
@@ -80,10 +82,12 @@ export class DebuggerDriver {
80
82
  },
81
83
  onNodeEnd: (data) => {
82
84
  this.ui.state.log(`<- node: ${data.nodeName}`);
85
+ this.ui.render();
83
86
  },
84
87
  onLLMCallStart: (data) => {
85
88
  const model = typeof data.model === "string" ? data.model : "unknown";
86
89
  this.ui.state.log(`Calling LLM: ${model}...`);
90
+ this.ui.render();
87
91
  },
88
92
  onLLMCallEnd: (data) => {
89
93
  const tokens = data.usage
@@ -91,12 +95,15 @@ export class DebuggerDriver {
91
95
  : "unknown tokens";
92
96
  const time = `${round(data.timeTaken)}ms`;
93
97
  this.ui.state.log(`LLM returned (${tokens}, ${time})`);
98
+ this.ui.render();
94
99
  },
95
100
  onToolCallStart: (data) => {
96
101
  this.ui.state.log(`Tool call: ${data.toolName}()`);
102
+ this.ui.render();
97
103
  },
98
104
  onToolCallEnd: (data) => {
99
105
  this.ui.state.log(`Tool done: ${data.toolName} (${round(data.timeTaken)}ms)`);
106
+ this.ui.render();
100
107
  },
101
108
  };
102
109
  }
@@ -381,6 +381,7 @@ export class DebuggerUI {
381
381
  // Format messages
382
382
  const content = threadData.messages
383
383
  .map((m) => {
384
+ // if its not a string,call json.strinfigy
384
385
  const truncated = m.content.length > 200 ? m.content.slice(0, 197) + "..." : m.content;
385
386
  return ` ${this.bold(`[${this.fmt(m.role)}]`)} ${this.fmt(truncated)}`;
386
387
  })
@@ -51,8 +51,12 @@ export declare const ts: {
51
51
  parenLeft?: boolean;
52
52
  parenRight?: boolean;
53
53
  }): TsBinOp;
54
- prop(object: TsNode, property: string): TsPropertyAccess;
55
- index(object: TsNode, property: TsNode): TsPropertyAccess;
54
+ prop(object: TsNode, property: string, opts?: {
55
+ optional?: boolean;
56
+ }): TsPropertyAccess;
57
+ index(object: TsNode, property: TsNode, opts?: {
58
+ optional?: boolean;
59
+ }): TsPropertyAccess;
56
60
  spread(expr: TsNode): TsSpread;
57
61
  id(name: string): TsIdentifier;
58
62
  str(value: string): TsStringLiteral;
@@ -166,11 +166,11 @@ export const ts = {
166
166
  parenRight: opts?.parenRight,
167
167
  };
168
168
  },
169
- prop(object, property) {
170
- return { kind: "propertyAccess", object, property, computed: false };
169
+ prop(object, property, opts) {
170
+ return { kind: "propertyAccess", object, property, computed: false, ...(opts?.optional && { optional: true }) };
171
171
  },
172
- index(object, property) {
173
- return { kind: "propertyAccess", object, property, computed: true };
172
+ index(object, property, opts) {
173
+ return { kind: "propertyAccess", object, property, computed: true, ...(opts?.optional && { optional: true }) };
174
174
  },
175
175
  spread(expr) {
176
176
  return { kind: "spread", expr };
@@ -9,7 +9,9 @@ export declare class TsChain {
9
9
  readonly node: TsNode;
10
10
  constructor(node: TsNode);
11
11
  /** Property access: .prop("foo") → ts.prop(this, "foo") */
12
- prop(name: string): TsChain;
12
+ prop(name: string, opts?: {
13
+ optional?: boolean;
14
+ }): TsChain;
13
15
  map(fn: TsNode): TsChain;
14
16
  /** Computed index: .index(expr) → ts.index(this, expr) */
15
17
  index(expr: TsNode): TsChain;
@@ -11,8 +11,8 @@ export class TsChain {
11
11
  this.node = node;
12
12
  }
13
13
  /** Property access: .prop("foo") → ts.prop(this, "foo") */
14
- prop(name) {
15
- return new TsChain(ts.prop(this.node, name));
14
+ prop(name, opts) {
15
+ return new TsChain(ts.prop(this.node, name, opts));
16
16
  }
17
17
  map(fn) {
18
18
  return new TsChain(ts.call(ts.prop(this.node, "map"), [fn]));
@@ -189,9 +189,11 @@ export function printTs(node, indent = 0) {
189
189
  case "propertyAccess": {
190
190
  const obj = printTs(node.object, indent);
191
191
  if (node.computed) {
192
- return `${obj}[${printTs(node.property, indent)}]`;
192
+ const prefix = node.optional ? "?." : "";
193
+ return `${obj}${prefix}[${printTs(node.property, indent)}]`;
193
194
  }
194
- return `${obj}.${node.property}`;
195
+ const dot = node.optional ? "?." : ".";
196
+ return `${obj}${dot}${node.property}`;
195
197
  }
196
198
  case "spread":
197
199
  return `...${printTs(node.expr, indent)}`;
@@ -153,6 +153,7 @@ export interface TsPropertyAccess {
153
153
  object: TsNode;
154
154
  property: string | TsNode;
155
155
  computed: boolean;
156
+ optional?: boolean;
156
157
  }
157
158
  export interface TsSpread {
158
159
  kind: "spread";
@@ -340,6 +340,73 @@ describe("valueAccessParser", () => {
340
340
  }
341
341
  });
342
342
  });
343
+ describe("optional chaining", () => {
344
+ it('should parse "foo?.bar" with optional property access', () => {
345
+ const result = valueAccessParser("foo?.bar");
346
+ expect(result.success).toBe(true);
347
+ if (result.success) {
348
+ expect(result.result).toEqualWithoutLoc({
349
+ type: "valueAccess",
350
+ base: { type: "variableName", value: "foo" },
351
+ chain: [{ kind: "property", name: "bar", optional: true }],
352
+ });
353
+ }
354
+ });
355
+ it('should parse "foo?.bar?.baz" with chained optional access', () => {
356
+ const result = valueAccessParser("foo?.bar?.baz");
357
+ expect(result.success).toBe(true);
358
+ if (result.success) {
359
+ expect(result.result).toEqualWithoutLoc({
360
+ type: "valueAccess",
361
+ base: { type: "variableName", value: "foo" },
362
+ chain: [
363
+ { kind: "property", name: "bar", optional: true },
364
+ { kind: "property", name: "baz", optional: true },
365
+ ],
366
+ });
367
+ }
368
+ });
369
+ it('should parse "foo?.bar.baz" mixing optional and regular access', () => {
370
+ const result = valueAccessParser("foo?.bar.baz");
371
+ expect(result.success).toBe(true);
372
+ if (result.success) {
373
+ expect(result.result).toEqualWithoutLoc({
374
+ type: "valueAccess",
375
+ base: { type: "variableName", value: "foo" },
376
+ chain: [
377
+ { kind: "property", name: "bar", optional: true },
378
+ { kind: "property", name: "baz" },
379
+ ],
380
+ });
381
+ }
382
+ });
383
+ it('should parse "arr?.[0]" with optional index access', () => {
384
+ const result = valueAccessParser("arr?.[0]");
385
+ expect(result.success).toBe(true);
386
+ if (result.success) {
387
+ expect(result.result).toEqualWithoutLoc({
388
+ type: "valueAccess",
389
+ base: { type: "variableName", value: "arr" },
390
+ chain: [{ kind: "index", index: { type: "number", value: "0" }, optional: true }],
391
+ });
392
+ }
393
+ });
394
+ it('should parse "obj?.method()" with optional method call', () => {
395
+ const result = valueAccessParser("obj?.method()");
396
+ expect(result.success).toBe(true);
397
+ if (result.success) {
398
+ expect(result.result).toEqualWithoutLoc({
399
+ type: "valueAccess",
400
+ base: { type: "variableName", value: "obj" },
401
+ chain: [{
402
+ kind: "methodCall",
403
+ functionCall: { type: "functionCall", functionName: "method", arguments: [] },
404
+ optional: true,
405
+ }],
406
+ });
407
+ }
408
+ });
409
+ });
343
410
  describe("failure cases", () => {
344
411
  it('should fail to parse ""', () => {
345
412
  expect(valueAccessParser("").success).toBe(false);
@@ -524,6 +524,68 @@ describe("binOpParser", () => {
524
524
  }
525
525
  });
526
526
  });
527
+ // Nullish coalescing operator
528
+ describe("nullish coalescing operator", () => {
529
+ it('should parse "a ?? b"', () => {
530
+ const result = binOpParser("a ?? b");
531
+ expect(result.success).toBe(true);
532
+ if (result.success) {
533
+ expect(result.result).toEqualWithoutLoc({
534
+ type: "binOpExpression",
535
+ operator: "??",
536
+ left: { type: "variableName", value: "a" },
537
+ right: { type: "variableName", value: "b" },
538
+ });
539
+ }
540
+ });
541
+ it('should parse "a ?? b ?? c" as left-associative', () => {
542
+ const result = binOpParser("a ?? b ?? c");
543
+ expect(result.success).toBe(true);
544
+ if (result.success) {
545
+ expect(result.result).toEqualWithoutLoc({
546
+ type: "binOpExpression",
547
+ operator: "??",
548
+ left: {
549
+ type: "binOpExpression",
550
+ operator: "??",
551
+ left: { type: "variableName", value: "a" },
552
+ right: { type: "variableName", value: "b" },
553
+ },
554
+ right: { type: "variableName", value: "c" },
555
+ });
556
+ }
557
+ });
558
+ it('should parse "a ?? 42"', () => {
559
+ const result = binOpParser("a ?? 42");
560
+ expect(result.success).toBe(true);
561
+ if (result.success) {
562
+ expect(result.result).toEqualWithoutLoc({
563
+ type: "binOpExpression",
564
+ operator: "??",
565
+ left: { type: "variableName", value: "a" },
566
+ right: { type: "number", value: "42" },
567
+ });
568
+ }
569
+ });
570
+ it('should parse ?? with lower precedence than comparison', () => {
571
+ const result = binOpParser("a == b ?? c");
572
+ expect(result.success).toBe(true);
573
+ if (result.success) {
574
+ // Should parse as (a == b) ?? c
575
+ expect(result.result).toEqualWithoutLoc({
576
+ type: "binOpExpression",
577
+ operator: "??",
578
+ left: {
579
+ type: "binOpExpression",
580
+ operator: "==",
581
+ left: { type: "variableName", value: "a" },
582
+ right: { type: "variableName", value: "b" },
583
+ },
584
+ right: { type: "variableName", value: "c" },
585
+ });
586
+ }
587
+ });
588
+ });
527
589
  // Failure cases
528
590
  describe("regex match operators", () => {
529
591
  const testCases = [
@@ -22,7 +22,7 @@ export const varNameChar = oneOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST
22
22
  However, because every agency code gets rendered in a template that imports some standard functions,
23
23
  the line numbers would be off if we didn't account for the template lines.
24
24
  */
25
- const AGENCY_TEMPLATE_OFFSET = 2;
25
+ const AGENCY_TEMPLATE_OFFSET = 3;
26
26
  /**
27
27
  * Wraps a parser to add a `loc` field from tarsec's withSpan.
28
28
  * Converts Span { start: Position, end: Position } to SourceLocation { line, col, start, end }.
@@ -409,27 +409,35 @@ export const functionCallParser = label("a function call", _functionCallParser);
409
409
  // =============================================================================
410
410
  // access.ts
411
411
  // =============================================================================
412
- // Parse a single chain element: .method(), .property, or [index]
412
+ // Parse "?." or "." returns true for optional, false for regular
413
+ const dotParser = or(map(str("?."), () => true), map(char("."), () => false));
414
+ // Parse a single chain element: .method(), ?.method(), .property, ?.property, [index], ?.[index]
413
415
  const dotMethodCallParser = (input) => {
414
- // First try: . followed by functionCall (name + parens)
415
- const dotResult = char(".")(input);
416
+ const dotResult = dotParser(input);
416
417
  if (!dotResult.success)
417
418
  return failure("expected dot", input);
418
- const fcResult = _functionCallParser(dotResult.rest);
419
+ const optional = dotResult.result;
420
+ const afterDot = dotResult.rest;
421
+ // First try: functionCall (name + parens)
422
+ const fcResult = _functionCallParser(afterDot);
419
423
  if (fcResult.success) {
420
- return success({ kind: "methodCall", functionCall: fcResult.result }, fcResult.rest);
424
+ return success({ kind: "methodCall", functionCall: fcResult.result, ...(optional && { optional: true }) }, fcResult.rest);
421
425
  }
422
- // Second try: . followed by just a property name
423
- const nameResult = variableNameParser(dotResult.rest);
426
+ // Second try: just a property name
427
+ const nameResult = variableNameParser(afterDot);
424
428
  if (nameResult.success) {
425
- return success({ kind: "property", name: nameResult.result.value }, nameResult.rest);
429
+ return success({ kind: "property", name: nameResult.result.value, ...(optional && { optional: true }) }, nameResult.rest);
426
430
  }
427
431
  return failure("expected property name or method call after dot", input);
428
432
  };
433
+ // Parse "?.[" or "[" — returns true for optional, false for regular
434
+ const bracketParser = or(map(str("?.["), () => true), map(char("["), () => false));
429
435
  const indexChainParser = (input) => {
430
- const parser = seqC(set("kind", "index"), char("["), optionalSpaces, capture(lazy(() => exprParser), "index"), optionalSpaces, char("]"));
436
+ const parser = seqC(capture(bracketParser, "optional"), optionalSpaces, capture(lazy(() => exprParser), "index"), optionalSpaces, char("]"));
431
437
  const result = parser(input);
432
- return result;
438
+ if (!result.success)
439
+ return result;
440
+ return success({ kind: "index", index: result.result.index, ...(result.result.optional && { optional: true }) }, result.rest);
433
441
  };
434
442
  const chainElementParser = or(dotMethodCallParser, indexChainParser);
435
443
  export const _valueAccessParser = (input) => {
@@ -607,8 +615,9 @@ export const exprParser = label("an expression", buildExpressionParser(atom, [
607
615
  [
608
616
  { op: wsOp("&&"), assoc: "left", apply: makeBinOp("&&") },
609
617
  ],
610
- // Precedence 1: logical OR
618
+ // Precedence 1: logical OR, nullish coalescing
611
619
  [
620
+ { op: wsOp("??"), assoc: "left", apply: makeBinOp("??") },
612
621
  { op: wsOp("||"), assoc: "left", apply: makeBinOp("||") },
613
622
  ],
614
623
  // Precedence 0: catch (unwrap Result with fallback)
@@ -40,6 +40,7 @@ export type CallbackMap = {
40
40
  functionName: string;
41
41
  args: Record<string, any>;
42
42
  isBuiltin: boolean;
43
+ moduleId: string;
43
44
  };
44
45
  onFunctionEnd: {
45
46
  functionName: string;
@@ -114,8 +114,10 @@ export async function respondToInterrupt(args) {
114
114
  execCtx.debuggerState = metadata.debugger;
115
115
  }
116
116
  let interruptData = interrupt.interruptData || {};
117
- if (interrupt.debugger) {
118
- // Debugger-generated interrupts don't carry tool-call data
117
+ if (interrupt.debugger && !interrupt.interruptData?.toolCall) {
118
+ // Debugger-generated interrupts don't carry tool-call data,
119
+ // unless the debug pause happened inside a tool call during an LLM call —
120
+ // in that case, keep interruptData so runPrompt can resume mid-conversation.
119
121
  interruptData = undefined;
120
122
  }
121
123
  else {
@@ -2,6 +2,7 @@ import * as smoltalk from "smoltalk";
2
2
  import { MessageThread } from "./state/messageThread.js";
3
3
  import { InterruptData } from "./interrupts.js";
4
4
  import type { RuntimeContext } from "./state/context.js";
5
+ import type { SourceLocationOpts } from "./state/checkpointStore.js";
5
6
  import { GraphState } from "./types.js";
6
7
  export interface ToolHandler {
7
8
  name: string;
@@ -20,4 +21,5 @@ export declare function runPrompt(args: {
20
21
  maxToolCallRounds?: number;
21
22
  interruptData?: InterruptData;
22
23
  removedTools?: string[];
24
+ checkpointInfo?: SourceLocationOpts;
23
25
  }): Promise<any>;
@@ -152,6 +152,7 @@ async function executeToolCalls({ toolCalls, toolHandlers, messages, ctx, client
152
152
  isToolCall: true,
153
153
  });
154
154
  const toolCallStartTime = performance.now();
155
+ ctx.enterToolCall();
155
156
  try {
156
157
  result = await handler.execute(...params);
157
158
  }
@@ -182,6 +183,9 @@ async function executeToolCalls({ toolCalls, toolHandlers, messages, ctx, client
182
183
  }
183
184
  continue;
184
185
  }
186
+ finally {
187
+ ctx.exitToolCall();
188
+ }
185
189
  // Tool returned a failure Result — handle retry logic
186
190
  if (isFailure(result)) {
187
191
  const errorMessage = typeof result.error === "string" ? result.error : String(result.error);
@@ -250,7 +254,7 @@ async function executeToolCalls({ toolCalls, toolHandlers, messages, ctx, client
250
254
  return { isInterrupt: false, messages };
251
255
  }
252
256
  export async function runPrompt(args) {
253
- const { ctx, prompt, responseFormat, maxToolCallRounds = 10, removedTools = [], } = args;
257
+ const { ctx, prompt, responseFormat, maxToolCallRounds = 10, removedTools = [], checkpointInfo, } = args;
254
258
  // Extract tool registry entries from clientConfig.tools and split into
255
259
  // definitions (for smoltalk) and handlers (for execution).
256
260
  const toolEntries = (args.clientConfig?.tools || []).map((entry) => entry);
@@ -330,13 +334,18 @@ export async function runPrompt(args) {
330
334
  messages: messages.getMessages(),
331
335
  model: clientConfig.model,
332
336
  });
333
- const checkpointId = ctx.checkpoints.create(ctx, {
334
- moduleId: "",
335
- scopeName: "",
336
- stepPath: "",
337
- });
338
- interrupt.checkpointId = checkpointId;
339
- interrupt.checkpoint = ctx.checkpoints.get(checkpointId);
337
+ if (interrupt.debugger === false) {
338
+ // For real user interrupts, create a checkpoint at the LLM call site
339
+ // so we can resume the conversation. Debug interrupts keep their
340
+ // original checkpoint from debugStep (pointing to the tool's source).
341
+ const checkpointId = ctx.checkpoints.create(ctx, {
342
+ moduleId: checkpointInfo?.moduleId ?? "",
343
+ scopeName: checkpointInfo?.scopeName ?? "",
344
+ stepPath: checkpointInfo?.stepPath ?? "",
345
+ });
346
+ interrupt.checkpointId = checkpointId;
347
+ interrupt.checkpoint = ctx.checkpoints.get(checkpointId);
348
+ }
340
349
  return interrupt;
341
350
  }
342
351
  const result = await _runPrompt({
@@ -1,6 +1,7 @@
1
1
  import type { State } from "./state/stateStack.js";
2
2
  import { StateStack } from "./state/stateStack.js";
3
3
  import type { RuntimeContext } from "./state/context.js";
4
+ import type { SourceLocationOpts } from "./state/checkpointStore.js";
4
5
  import type { HandlerFn } from "./types.js";
5
6
  /**
6
7
  * Runner centralizes step execution logic for generated Agency code.
@@ -31,6 +32,8 @@ export declare class Runner {
31
32
  });
32
33
  /** The current step path as a string, e.g. "1_0_2" */
33
34
  key(): string;
35
+ /** Return checkpoint metadata for the current step. */
36
+ getCheckpointInfo(): SourceLocationOpts;
34
37
  private getCounter;
35
38
  private setCounter;
36
39
  halt(result: any): void;
@@ -36,6 +36,14 @@ export class Runner {
36
36
  key() {
37
37
  return this.path.join("_");
38
38
  }
39
+ /** Return checkpoint metadata for the current step. */
40
+ getCheckpointInfo() {
41
+ return {
42
+ moduleId: this.moduleId,
43
+ scopeName: this.scopeName,
44
+ stepPath: this.path.join("."),
45
+ };
46
+ }
39
47
  getCounter() {
40
48
  if (this.path.length === 0)
41
49
  return this.frame.step;
@@ -84,6 +92,8 @@ export class Runner {
84
92
  async maybeDebugHook(id, label = null, isUserAdded = false) {
85
93
  if (!this.ctx.debuggerState && !this.ctx.traceWriter)
86
94
  return false;
95
+ if (this.ctx.isInsideToolCall())
96
+ return false;
87
97
  // On resume after a debug pause, skip the hook.
88
98
  // Don't delete the flag yet — step() will clean it up after the
89
99
  // callback completes. If the callback halts (nested interrupt),
@@ -40,6 +40,7 @@ export declare class Checkpoint implements SourceLocation {
40
40
  get location(): SourceLocation;
41
41
  getCurrentFrame(): import("./stateStack.js").StateJSON | undefined;
42
42
  getThreadMessages(): ThreadMessages | null;
43
+ private getContentFromMessage;
43
44
  getGlobalsForModule(): Record<string, any> | null;
44
45
  getFilename(): string;
45
46
  pathEquals(other: Checkpoint): boolean;
@@ -61,16 +61,27 @@ export class Checkpoint {
61
61
  const messages = activeThread.messages.map((m) => ({
62
62
  role: m.role ?? "unknown",
63
63
  // smoltalk content can be string, TextPart[], or null
64
- content: typeof m.content === "string"
65
- ? m.content
66
- : Array.isArray(m.content)
67
- ? m.content.map((p) => p.text ?? "").join("")
68
- : m.content == null
69
- ? ""
70
- : String(m.content),
64
+ content: this.getContentFromMessage(m),
71
65
  }));
72
66
  return { threadId: activeId, messages };
73
67
  }
68
+ getContentFromMessage(message) {
69
+ if (typeof message.content === "string") {
70
+ return message.content;
71
+ }
72
+ else if (Array.isArray(message.content)) {
73
+ return message.content.map((part) => part.text ?? "").join("");
74
+ }
75
+ else if (message.content == null) {
76
+ if (message.role === "assistant" && message.toolCalls) {
77
+ return message.toolCalls
78
+ .map((toolCall) => `Tool call: ${toolCall.name}(${JSON.stringify(toolCall.arguments)})`)
79
+ .join("\n");
80
+ }
81
+ return "(no content)";
82
+ }
83
+ return JSON.stringify(message.content);
84
+ }
74
85
  getGlobalsForModule() {
75
86
  return this.globals.store?.[this.moduleId] ?? null;
76
87
  }
@@ -261,7 +261,7 @@ describe("Checkpoint", () => {
261
261
  });
262
262
  const result = cp.getThreadMessages();
263
263
  expect(result.messages).toEqual([
264
- { role: "assistant", content: "" },
264
+ { role: "assistant", content: "(no content)" },
265
265
  ]);
266
266
  });
267
267
  it("should concatenate TextPart[] content", () => {
@@ -23,6 +23,7 @@ export declare class RuntimeContext<T> {
23
23
  _skipNextCheckpoint: boolean;
24
24
  _pendingArgOverrides?: Record<string, any>;
25
25
  _restoreCount: number;
26
+ _toolCallDepth: number;
26
27
  debuggerState: DebuggerState | null;
27
28
  traceWriter: TraceWriter | null;
28
29
  statelogClient: StatelogClient;
@@ -40,6 +41,9 @@ export declare class RuntimeContext<T> {
40
41
  createExecutionContext(): RuntimeContext<T>;
41
42
  pushHandler(fn: HandlerFn): void;
42
43
  popHandler(): void;
44
+ enterToolCall(): void;
45
+ exitToolCall(): void;
46
+ isInsideToolCall(): boolean;
43
47
  registerClass(name: string, cls: ClassRegistry[string]): void;
44
48
  forkStack(): StateStack;
45
49
  /** Sever references held by an execution context so GC can reclaim them. */