agency-lang 0.7.5 → 0.7.7

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.
@@ -1257,7 +1257,7 @@ node main() {
1257
1257
  policy: {
1258
1258
  type: "string",
1259
1259
  optional: true,
1260
- description: "Approval policy for this run: a built-in name (minimal, recommended, with-writes, with-bash) or a path to a policy JSON file. with-writes/with-bash auto-approve writes (and, for with-bash, shell) scoped to the current directory and its children. Overrides the saved policy without persisting it. Pass --policy with no value to list the built-in policies."
1260
+ description: "Approval policy for this run: a built-in name (minimal, recommended, with-writes, approve-all) or a path to a policy JSON file. with-writes auto-approves file writes and git changes scoped to the current directory and its children; approve-all approves EVERY interrupt with no scoping (sandbox use only). Overrides the saved policy without persisting it. Pass --policy with no value to list the built-in policies."
1261
1261
  },
1262
1262
  local: {
1263
1263
  type: "string",
@@ -7354,7 +7354,7 @@ graph.node("main", async (__state) => {
7354
7354
  "policy": {
7355
7355
  "type": `string`,
7356
7356
  "optional": true,
7357
- "description": `Approval policy for this run: a built-in name (minimal, recommended, with-writes, with-bash) or a path to a policy JSON file. with-writes/with-bash auto-approve writes (and, for with-bash, shell) scoped to the current directory and its children. Overrides the saved policy without persisting it. Pass --policy with no value to list the built-in policies.`
7357
+ "description": `Approval policy for this run: a built-in name (minimal, recommended, with-writes, approve-all) or a path to a policy JSON file. with-writes auto-approves file writes and git changes scoped to the current directory and its children; approve-all approves EVERY interrupt with no scoping (sandbox use only). Overrides the saved policy without persisting it. Pass --policy with no value to list the built-in policies.`
7358
7358
  },
7359
7359
  "local": {
7360
7360
  "type": `string`,
@@ -163,19 +163,18 @@ export def withWritesPolicy(baseDir: string) {
163
163
  }
164
164
  }
165
165
 
166
- // `with-writes` plus auto-approval of `std::bash`, scoped to `baseDir`
167
- // on the command's `cwd`. NOTE: a shell command can still reach outside
168
- // `baseDir` via an absolute path or `cd` inside the command string —
169
- // the policy matches the declared `cwd`, not the shell's actual effects,
170
- // so this is best-effort confinement, not a hard sandbox. It exists so
171
- // autonomous / sandbox runs (e.g. a benchmark container) can drive the
172
- // shell without a human answering every approval.
173
- export def withBashPolicy(baseDir: string) {
174
- const scope = dirScope(baseDir)
175
- return {
176
- ...withWritesPolicy(baseDir),
177
- "std::bash": [{ match: { cwd: scope }, action: "approve" }]
178
- }
166
+ // Approve EVERY interrupt reads, writes, shell, git, anything, anywhere,
167
+ // with no path scoping. The `"*"` wildcard is a catch-all that `checkPolicy`
168
+ // consults for any effect without a more specific rule (see
169
+ // lib/runtime/policy.ts). This is NOT confined in any way: it will approve
170
+ // writes and shell commands outside the working directory, deletes, network
171
+ // calls everything. Use it ONLY in a disposable sandbox (e.g. a benchmark
172
+ // container) where the whole environment is throwaway. `std::bash` cannot be
173
+ // meaningfully confined to a directory anyway (a command can `cd` or use
174
+ // absolute paths), so for autonomous sandbox runs a blanket approve-all is
175
+ // both simpler and more honest than a best-effort per-effect scope.
176
+ export static const approveAllPolicy = {
177
+ "*": [{ action: "approve" }]
179
178
  }
180
179
 
181
180
  // Built-in policies accepted by the `--policy` flag (anything else is
@@ -196,8 +195,8 @@ export static const BUILTIN_POLICIES = [
196
195
  description: "recommended + auto-approve file writes and git changes, scoped to the current directory and its children."
197
196
  },
198
197
  {
199
- name: "with-bash",
200
- description: "with-writes + auto-approve shell commands, scoped to the current directory. Best-effort confinement use in disposable/sandbox runs."
198
+ name: "approve-all",
199
+ description: "Approve EVERY interrupt reads, writes, shell, git, anywhere, no scoping. UNSAFE outside a disposable sandbox."
201
200
  },
202
201
  ]
203
202
 
@@ -221,8 +220,8 @@ export def builtinPolicy(name: string, baseDir: string) {
221
220
  if (name == "with-writes") {
222
221
  return withWritesPolicy(baseDir)
223
222
  }
224
- if (name == "with-bash") {
225
- return withBashPolicy(baseDir)
223
+ if (name == "approve-all") {
224
+ return approveAllPolicy
226
225
  }
227
226
  return null
228
227
  }
@@ -164,6 +164,7 @@ let __staticInitPromise = null;
164
164
  let AGENCY_SAFE_SUBCOMMANDS = __UNINIT_STATIC;
165
165
  let minimalAutoApprovePolicy = __UNINIT_STATIC;
166
166
  let recommendedAutoApprovePolicy = __UNINIT_STATIC;
167
+ let approveAllPolicy = __UNINIT_STATIC;
167
168
  let BUILTIN_POLICIES = __UNINIT_STATIC;
168
169
  async function __initializeStatic(__ctx) {
169
170
  if (__staticInitPromise) {
@@ -264,6 +265,11 @@ async function __initializeStatic(__ctx) {
264
265
  "action": `approve`
265
266
  }]
266
267
  });
268
+ approveAllPolicy = __deepFreeze({
269
+ "*": [{
270
+ "action": `approve`
271
+ }]
272
+ });
267
273
  BUILTIN_POLICIES = __deepFreeze([{
268
274
  "name": `recommended`,
269
275
  "description": `Auto-approve reads and web/search; prompt for writes, shell, and git changes. (Default.)`
@@ -274,8 +280,8 @@ async function __initializeStatic(__ctx) {
274
280
  "name": `with-writes`,
275
281
  "description": `recommended + auto-approve file writes and git changes, scoped to the current directory and its children.`
276
282
  }, {
277
- "name": `with-bash`,
278
- "description": `with-writes + auto-approve shell commands, scoped to the current directory. Best-effort confinement \u2014 use in disposable/sandbox runs.`
283
+ "name": `approve-all`,
284
+ "description": `Approve EVERY interrupt \u2014 reads, writes, shell, git, anywhere, no scoping. UNSAFE outside a disposable sandbox.`
279
285
  }]);
280
286
  })();
281
287
  return __staticInitPromise;
@@ -285,6 +291,7 @@ function __getStaticVars() {
285
291
  AGENCY_SAFE_SUBCOMMANDS,
286
292
  minimalAutoApprovePolicy,
287
293
  recommendedAutoApprovePolicy,
294
+ approveAllPolicy,
288
295
  BUILTIN_POLICIES
289
296
  };
290
297
  }
@@ -728,144 +735,6 @@ const withWritesPolicy = __AgencyFunction.create({
728
735
  safe: false,
729
736
  exported: true
730
737
  }, __toolRegistry);
731
- async function __withBashPolicy_impl(baseDir) {
732
- const __setupData = setupFunction();
733
- const __stack = __setupData.stack;
734
- const __step = __setupData.step;
735
- const __self = __setupData.self;
736
- const __ctx = getRuntimeContext().ctx;
737
- let __forked;
738
- let __functionCompleted = false;
739
- if (!__globals().isInitialized("dist/lib/agents/agency-agent/lib/defaultPolicy.agency")) {
740
- await __initializeGlobals(__ctx);
741
- }
742
- let __funcStartTime = performance.now();
743
- __stack.args["baseDir"] = baseDir;
744
- __self.__retryable = __self.__retryable ?? true;
745
- const runner = new Runner(__ctx, __stack, { state: __stack, moduleId: "dist/lib/agents/agency-agent/lib/defaultPolicy.agency", scopeName: "withBashPolicy", threads: __setupData.threads });
746
- let __resultCheckpointId = -1;
747
- if (__ctx._pendingArgOverrides) {
748
- const __overrides = __ctx._pendingArgOverrides;
749
- __ctx._pendingArgOverrides = void 0;
750
- if ("baseDir" in __overrides) {
751
- baseDir = __overrides["baseDir"];
752
- __stack.args["baseDir"] = baseDir;
753
- }
754
- }
755
- try {
756
- await agencyStore.run({
757
- ...getRuntimeContext(),
758
- ctx: __ctx,
759
- stack: __setupData.stateStack,
760
- threads: __setupData.threads
761
- }, async () => {
762
- await runner.hook(0, async () => {
763
- await callHook({
764
- name: "onFunctionStart",
765
- data: {
766
- functionName: "withBashPolicy",
767
- args: {
768
- baseDir
769
- },
770
- moduleId: "dist/lib/agents/agency-agent/lib/defaultPolicy.agency"
771
- }
772
- });
773
- });
774
- await runner.step(1, async (runner2) => {
775
- __stack.locals.scope = await __call(dirScope, {
776
- type: "positional",
777
- args: [__stack.args.baseDir]
778
- });
779
- if (hasInterrupts(__stack.locals.scope)) {
780
- await getRuntimeContext().ctx.pendingPromises.awaitAll();
781
- runner2.halt(__stack.locals.scope);
782
- return;
783
- }
784
- });
785
- await runner.step(2, async (runner2) => {
786
- __functionCompleted = true;
787
- runner2.halt({
788
- ...await __call(withWritesPolicy, {
789
- type: "positional",
790
- args: [__stack.args.baseDir]
791
- }),
792
- "std::bash": [{
793
- "match": {
794
- "cwd": __stack.locals.scope
795
- },
796
- "action": `approve`
797
- }]
798
- });
799
- return;
800
- });
801
- });
802
- if (runner.halted) {
803
- if (isFailure(runner.haltResult)) {
804
- runner.haltResult.retryable = runner.haltResult.retryable && __self.__retryable;
805
- }
806
- return runner.haltResult;
807
- }
808
- } catch (__error) {
809
- if (__error instanceof RestoreSignal) {
810
- throw __error;
811
- }
812
- if (__error instanceof AgencyAbort) {
813
- throw __error;
814
- }
815
- {
816
- const __errMsg = __error instanceof Error ? __error.message : String(__error);
817
- const __errStack = __error instanceof Error && __error.stack ? __error.stack : "";
818
- const __log = __createLogger(__ctx.logLevel);
819
- __log.error("Function withBashPolicy threw an exception (converted to Failure): " + __errMsg);
820
- if (__errStack) __log.error(__errStack);
821
- __ctx.statelogClient?.error?.({
822
- errorType: "runtimeError",
823
- message: __errMsg,
824
- functionName: "withBashPolicy",
825
- retryable: __self.__retryable
826
- });
827
- }
828
- return failure(
829
- __error instanceof Error ? __error.message : String(__error),
830
- {
831
- checkpoint: getRuntimeContext().ctx.getResultCheckpoint(),
832
- retryable: __self.__retryable,
833
- functionName: "withBashPolicy",
834
- args: __stack.args
835
- }
836
- );
837
- } finally {
838
- __stateStack()?.pop();
839
- if (__functionCompleted) {
840
- await callHook({
841
- name: "onFunctionEnd",
842
- data: {
843
- functionName: "withBashPolicy",
844
- timeTaken: performance.now() - __funcStartTime
845
- }
846
- });
847
- }
848
- }
849
- }
850
- const withBashPolicy = __AgencyFunction.create({
851
- name: "withBashPolicy",
852
- module: "dist/lib/agents/agency-agent/lib/defaultPolicy.agency",
853
- fn: __withBashPolicy_impl,
854
- params: [{
855
- name: "baseDir",
856
- hasDefault: false,
857
- defaultValue: void 0,
858
- variadic: false,
859
- isFunctionTyped: false
860
- }],
861
- toolDefinition: {
862
- name: "withBashPolicy",
863
- description: "No description provided.",
864
- schema: z.object({ "baseDir": z.string() })
865
- },
866
- safe: false,
867
- exported: true
868
- }, __toolRegistry);
869
738
  async function __builtinPolicyNames_impl() {
870
739
  const __setupData = setupFunction();
871
740
  const __stack = __setupData.stack;
@@ -1081,14 +950,11 @@ async function __builtinPolicy_impl(name, baseDir) {
1081
950
  ]);
1082
951
  await runner.ifElse(4, [
1083
952
  {
1084
- condition: async () => __eq(__stack.args.name, `with-bash`),
953
+ condition: async () => __eq(__stack.args.name, `approve-all`),
1085
954
  body: async (runner2) => {
1086
955
  await runner2.step(0, async (runner3) => {
1087
956
  __functionCompleted = true;
1088
- runner3.halt(await __call(withBashPolicy, {
1089
- type: "positional",
1090
- args: [__stack.args.baseDir]
1091
- }));
957
+ runner3.halt(__readStatic(approveAllPolicy, "approveAllPolicy", "dist/lib/agents/agency-agent/lib/defaultPolicy.agency"));
1092
958
  return;
1093
959
  });
1094
960
  }
@@ -1174,7 +1040,7 @@ const builtinPolicy = __AgencyFunction.create({
1174
1040
  exported: true
1175
1041
  }, __toolRegistry);
1176
1042
  var stdin_default = graph;
1177
- const __sourceMap = { "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:agencyExecApproveRules": { "1": { "line": 20, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:__block_0": { "1.0": { "line": 21, "col": 4 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:dirScope": { "1": { "line": 130, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:withWritesPolicy": { "1": { "line": 141, "col": 2 }, "2": { "line": 142, "col": 2 }, "3": { "line": 143, "col": 2 }, "4": { "line": 144, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:withBashPolicy": { "1": { "line": 173, "col": 2 }, "2": { "line": 174, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:builtinPolicyNames": { "1": { "line": 205, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:__block_1": { "1.0": { "line": 206, "col": 4 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:builtinPolicy": { "1": { "line": 214, "col": 2 }, "2": { "line": 217, "col": 2 }, "3": { "line": 220, "col": 2 }, "4": { "line": 223, "col": 2 }, "5": { "line": 226, "col": 2 }, "1.0": { "line": 215, "col": 4 }, "2.0": { "line": 218, "col": 4 }, "3.0": { "line": 221, "col": 4 }, "4.0": { "line": 224, "col": 4 } } };
1043
+ const __sourceMap = { "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:agencyExecApproveRules": { "1": { "line": 20, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:__block_0": { "1.0": { "line": 21, "col": 4 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:dirScope": { "1": { "line": 130, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:withWritesPolicy": { "1": { "line": 141, "col": 2 }, "2": { "line": 142, "col": 2 }, "3": { "line": 143, "col": 2 }, "4": { "line": 144, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:builtinPolicyNames": { "1": { "line": 204, "col": 2 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:__block_1": { "1.0": { "line": 205, "col": 4 } }, "dist/lib/agents/agency-agent/lib/defaultPolicy.agency:builtinPolicy": { "1": { "line": 213, "col": 2 }, "2": { "line": 216, "col": 2 }, "3": { "line": 219, "col": 2 }, "4": { "line": 222, "col": 2 }, "5": { "line": 225, "col": 2 }, "1.0": { "line": 214, "col": 4 }, "2.0": { "line": 217, "col": 4 }, "3.0": { "line": 220, "col": 4 }, "4.0": { "line": 223, "col": 4 } } };
1178
1044
  export {
1179
1045
  BUILTIN_POLICIES,
1180
1046
  __getCheckpoints,
@@ -1186,6 +1052,7 @@ export {
1186
1052
  __toolRegistry,
1187
1053
  agencyExecApproveRules,
1188
1054
  approve,
1055
+ approveAllPolicy,
1189
1056
  builtinPolicy,
1190
1057
  builtinPolicyNames,
1191
1058
  stdin_default as default,
@@ -1199,6 +1066,5 @@ export {
1199
1066
  reject,
1200
1067
  respondToInterrupts,
1201
1068
  rewindFrom,
1202
- withBashPolicy,
1203
1069
  withWritesPolicy
1204
1070
  };
@@ -6,13 +6,25 @@ export const PolicyRuleSchema = z.object({
6
6
  });
7
7
  export const PolicySchema = z.record(z.string(), z.array(PolicyRuleSchema));
8
8
  export function checkPolicy(policy, interrupt) {
9
+ // Effect-specific rules take precedence over the wildcard.
9
10
  const rules = policy[interrupt.effect];
10
- if (!rules) {
11
- return { type: "propagate" };
11
+ if (rules) {
12
+ for (const rule of rules) {
13
+ if (matchesRule(rule, interrupt)) {
14
+ return { type: rule.action };
15
+ }
16
+ }
12
17
  }
13
- for (const rule of rules) {
14
- if (matchesRule(rule, interrupt)) {
15
- return { type: rule.action };
18
+ // Wildcard catch-all: the `"*"` effect key applies to any interrupt whose
19
+ // own effect had no matching rule. This is how an "approve-all" policy
20
+ // covers effects it doesn't enumerate (a plain per-effect map would
21
+ // `propagate` — i.e. prompt — on anything unlisted).
22
+ const wildcard = policy["*"];
23
+ if (wildcard) {
24
+ for (const rule of wildcard) {
25
+ if (matchesRule(rule, interrupt)) {
26
+ return { type: rule.action };
27
+ }
16
28
  }
17
29
  }
18
30
  return { type: "propagate" };
@@ -111,6 +111,45 @@ describe("checkPolicy", () => {
111
111
  const result = checkPolicy(policy, { effect: "test::x", message: "", data: {}, origin: "" });
112
112
  expect(result).toEqual({ type: "reject" });
113
113
  });
114
+ describe('"*" wildcard catch-all', () => {
115
+ it("approve-all: applies to every effect, including unlisted ones", () => {
116
+ const policy = { "*": [{ action: "approve" }] };
117
+ for (const effect of ["std::write", "std::bash", "std::remove", "anything::else"]) {
118
+ expect(checkPolicy(policy, { effect, message: "", data: { dir: "/etc" }, origin: "" }))
119
+ .toEqual({ type: "approve" });
120
+ }
121
+ });
122
+ it("effect-specific rules take precedence over the wildcard", () => {
123
+ const policy = {
124
+ "std::bash": [{ action: "reject" }],
125
+ "*": [{ action: "approve" }],
126
+ };
127
+ expect(checkPolicy(policy, { effect: "std::bash", message: "", data: {}, origin: "" }))
128
+ .toEqual({ type: "reject" });
129
+ // An effect with no specific rule falls through to the wildcard.
130
+ expect(checkPolicy(policy, { effect: "std::write", message: "", data: {}, origin: "" }))
131
+ .toEqual({ type: "approve" });
132
+ });
133
+ it("falls back to the wildcard when a specific effect's rules all miss", () => {
134
+ const policy = {
135
+ "std::write": [{ match: { dir: "/app/**" }, action: "approve" }],
136
+ "*": [{ action: "reject" }],
137
+ };
138
+ // Inside /app: matched by the specific rule.
139
+ expect(checkPolicy(policy, { effect: "std::write", message: "", data: { dir: "/app/x" }, origin: "" }))
140
+ .toEqual({ type: "approve" });
141
+ // Outside /app: the specific rule misses, so the wildcard rejects.
142
+ expect(checkPolicy(policy, { effect: "std::write", message: "", data: { dir: "/etc/x" }, origin: "" }))
143
+ .toEqual({ type: "reject" });
144
+ });
145
+ it("still propagates when neither the effect nor the wildcard matches", () => {
146
+ const policy = {
147
+ "*": [{ match: { dir: "/app/**" }, action: "approve" }],
148
+ };
149
+ expect(checkPolicy(policy, { effect: "std::write", message: "", data: { dir: "/etc/x" }, origin: "" }))
150
+ .toEqual({ type: "propagate" });
151
+ });
152
+ });
114
153
  describe("./ prefix normalization (picomatch workaround)", () => {
115
154
  // picomatch.isMatch returns false for patterns starting with `./`
116
155
  // when combined with `**` or brace expansions — e.g.
@@ -1,5 +1,6 @@
1
1
  import * as smoltalk from "smoltalk";
2
2
  import { UserContentInput } from "smoltalk";
3
+ import { type FuncParam } from "./agencyFunction.js";
3
4
  import type { RetryPolicy, RetryConfig, LLMRetryReason } from "./llmRetry.js";
4
5
  import type { NormalizedLLMError } from "./llmClient.js";
5
6
  import type { SourceLocationOpts } from "./state/checkpointStore.js";
@@ -26,6 +27,17 @@ export declare function redactMessagesForLog(messages: MessageThread): smoltalk.
26
27
  /** A prompt with attachment payloads redacted, preserving its string-or-array
27
28
  * shape, for statelog / hook data. */
28
29
  export declare function redactPromptForLog(p: string | UserContentInput): string | UserContentInput;
30
+ /** LLMs routinely emit an explicit `null` for an optional tool argument
31
+ * they chose not to set (e.g. `bash(command, cwd: null, timeout: null)`).
32
+ * Agency default-parameter values only fill `undefined`, so a `null` would
33
+ * sail past the default into the function body — `bash(cwd: null)` reaches
34
+ * `applyAgentCwd` → `path.resolve(base, null)` and throws. Drop any argument
35
+ * whose value is `null` when its parameter declares a default, so the call
36
+ * behaves exactly as if the LLM had omitted the key (the default applies).
37
+ * Params without a default keep their value untouched: a required arg passed
38
+ * `null` still surfaces its normal type error for the model to correct, and
39
+ * an intentionally-nullable param is left alone. */
40
+ declare function dropNullDefaultedArgs(args: Record<string, any> | null | undefined, params: readonly FuncParam[]): Record<string, any>;
29
41
  /** Coerce an arbitrary tool result to the string the LLM would see.
30
42
  * Strings pass through; everything else is JSON-stringified, with a
31
43
  * `String()` fallback for values JSON can't represent (e.g. circular). */
@@ -92,6 +104,7 @@ export declare const _internal: {
92
104
  assertUniqueToolNames: typeof assertUniqueToolNames;
93
105
  armCallTimeout: typeof armCallTimeout;
94
106
  runWithRetry: typeof runWithRetry;
107
+ dropNullDefaultedArgs: typeof dropNullDefaultedArgs;
95
108
  };
96
109
  export declare function runPrompt(args: {
97
110
  prompt: string | UserContentInput;
@@ -77,6 +77,25 @@ async function dispatchLLMRequest({ ctx, promptConfig, prompt, stream, stateStac
77
77
  toolCalls: response.value.toolCalls || [],
78
78
  };
79
79
  }
80
+ /** LLMs routinely emit an explicit `null` for an optional tool argument
81
+ * they chose not to set (e.g. `bash(command, cwd: null, timeout: null)`).
82
+ * Agency default-parameter values only fill `undefined`, so a `null` would
83
+ * sail past the default into the function body — `bash(cwd: null)` reaches
84
+ * `applyAgentCwd` → `path.resolve(base, null)` and throws. Drop any argument
85
+ * whose value is `null` when its parameter declares a default, so the call
86
+ * behaves exactly as if the LLM had omitted the key (the default applies).
87
+ * Params without a default keep their value untouched: a required arg passed
88
+ * `null` still surfaces its normal type error for the model to correct, and
89
+ * an intentionally-nullable param is left alone. */
90
+ function dropNullDefaultedArgs(args, params) {
91
+ const out = { ...args };
92
+ for (const param of params) {
93
+ if (param.hasDefault && out[param.name] === null) {
94
+ delete out[param.name];
95
+ }
96
+ }
97
+ return out;
98
+ }
80
99
  /** Default cap on characters of a single tool result fed back to the
81
100
  * LLM. A recursive `ls`/`grep` can return megabytes; without a cap one
82
101
  * tool call can blow the context window. The FULL result is still
@@ -273,6 +292,7 @@ export const _internal = {
273
292
  assertUniqueToolNames,
274
293
  armCallTimeout,
275
294
  runWithRetry,
295
+ dropNullDefaultedArgs,
276
296
  };
277
297
  async function _runPrompt({ ctx, messages, tools, prompt, responseFormat, clientConfig, stateStack, retryPolicy, }) {
278
298
  if (ctx.isCancelled(stateStack)) {
@@ -908,7 +928,7 @@ export async function runPrompt(args) {
908
928
  // uniformly by completedSteps inside b.step (start/invoke/end
909
929
  // each get marked done on success and skipped on resume).
910
930
  const branchStack = stack.getOrCreateBranch(branchKey).stack;
911
- const namedArgs = { ...toolCall.arguments };
931
+ const namedArgs = dropNullDefaultedArgs(toolCall.arguments, handler.params);
912
932
  await b.step(`round.${round}.tool.${callSlug}.start`, async () => {
913
933
  // Pass `branchStack` so scoped callbacks registered inside
914
934
  // the branch's frame chain are discovered by
@@ -246,3 +246,41 @@ describe("runWithRetry", () => {
246
246
  await expect(_internal.runWithRetry(dispatch, policy, parent.signal, noHooks, normalize)).rejects.toSatisfy((e) => readCause(e)?.kind === "userInterrupt");
247
247
  });
248
248
  });
249
+ describe("dropNullDefaultedArgs", () => {
250
+ const P = (name, hasDefault) => ({
251
+ name,
252
+ hasDefault,
253
+ defaultValue: undefined,
254
+ variadic: false,
255
+ });
256
+ it("drops a null argument when the param has a default (LLM omitted it)", () => {
257
+ // Mirrors the real bug: bash(command, cwd: null) — cwd has a default of "".
258
+ const params = [P("command", false), P("cwd", true), P("timeout", true)];
259
+ const out = _internal.dropNullDefaultedArgs({ command: "ls", cwd: null, timeout: null }, params);
260
+ expect(out).toEqual({ command: "ls" });
261
+ expect("cwd" in out).toBe(false);
262
+ expect("timeout" in out).toBe(false);
263
+ });
264
+ it("keeps a null argument when the param has NO default", () => {
265
+ // A required (or intentionally-nullable) param keeps its value so the
266
+ // normal type error still surfaces for the model to correct.
267
+ const params = [P("target", false)];
268
+ expect(_internal.dropNullDefaultedArgs({ target: null }, params))
269
+ .toEqual({ target: null });
270
+ });
271
+ it("leaves non-null values untouched, including falsy ones", () => {
272
+ const params = [P("cwd", true), P("count", true), P("flag", true)];
273
+ const out = _internal.dropNullDefaultedArgs({ cwd: "/app", count: 0, flag: false }, params);
274
+ expect(out).toEqual({ cwd: "/app", count: 0, flag: false });
275
+ });
276
+ it("handles null/undefined argument objects", () => {
277
+ expect(_internal.dropNullDefaultedArgs(null, [P("cwd", true)])).toEqual({});
278
+ expect(_internal.dropNullDefaultedArgs(undefined, [P("cwd", true)])).toEqual({});
279
+ });
280
+ it("does not drop keys absent from the param list", () => {
281
+ // An arg with no matching param (e.g. an LLM hallucinated key) is left
282
+ // as-is; only declared defaulted params are considered.
283
+ const out = _internal.dropNullDefaultedArgs({ extra: null }, [P("cwd", true)]);
284
+ expect(out).toEqual({ extra: null });
285
+ });
286
+ });
@@ -1 +1 @@
1
- export declare const VERSION = "0.7.5";
1
+ export declare const VERSION = "0.7.7";
@@ -1 +1 @@
1
- export const VERSION = "0.7.5";
1
+ export const VERSION = "0.7.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agency-lang",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "The Agency language",
5
5
  "main": "lib/index.js",
6
6
  "bin": {