agency-lang 0.7.6 → 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.
@@ -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.6";
1
+ export declare const VERSION = "0.7.7";
@@ -1 +1 @@
1
- export const VERSION = "0.7.6";
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.6",
3
+ "version": "0.7.7",
4
4
  "description": "The Agency language",
5
5
  "main": "lib/index.js",
6
6
  "bin": {