agency-lang 0.7.1 → 0.7.2

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 (39) hide show
  1. package/dist/lib/agents/agency-agent/agent.agency +48 -2
  2. package/dist/lib/agents/agency-agent/agent.js +120 -20
  3. package/dist/lib/agents/agency-agent/shared.agency +22 -0
  4. package/dist/lib/agents/agency-agent/shared.js +158 -1
  5. package/dist/lib/agents/agency-agent/tests/oneShotRounds.agency +20 -0
  6. package/dist/lib/agents/agency-agent/tests/oneShotRounds.js +555 -0
  7. package/dist/lib/agents/agency-agent/tests/oneShotRounds.test.json +9 -0
  8. package/dist/lib/cli/doctor.d.ts +8 -0
  9. package/dist/lib/cli/doctor.js +21 -13
  10. package/dist/lib/cli/doctor.test.js +34 -0
  11. package/dist/lib/cli/help.js +1 -5
  12. package/dist/lib/cli/runBundledAgent.d.ts +17 -1
  13. package/dist/lib/cli/runBundledAgent.js +73 -2
  14. package/dist/lib/cli/runBundledAgent.test.d.ts +1 -0
  15. package/dist/lib/cli/runBundledAgent.test.js +96 -0
  16. package/dist/lib/config.d.ts +42 -33
  17. package/dist/lib/config.js +108 -6
  18. package/dist/lib/config.test.js +121 -2
  19. package/dist/lib/parseCache.js +8 -13
  20. package/dist/lib/parseCache.test.js +5 -8
  21. package/dist/lib/parser.js +5 -12
  22. package/dist/lib/preprocessors/typescriptPreprocessor.d.ts +0 -28
  23. package/dist/lib/preprocessors/typescriptPreprocessor.js +0 -245
  24. package/dist/lib/runtime/configOverrides.d.ts +17 -16
  25. package/dist/lib/runtime/configOverrides.js +27 -16
  26. package/dist/lib/runtime/configOverrides.test.js +116 -2
  27. package/dist/lib/runtime/prompt.js +7 -3
  28. package/dist/lib/runtime/state/context.js +9 -2
  29. package/dist/lib/stdlib/llm.d.ts +1 -0
  30. package/dist/lib/stdlib/version.d.ts +1 -1
  31. package/dist/lib/stdlib/version.js +1 -1
  32. package/dist/scripts/agency.d.ts +2 -0
  33. package/dist/scripts/agency.js +55 -12
  34. package/dist/scripts/agency.test.js +73 -1
  35. package/package.json +1 -1
  36. package/stdlib/llm.agency +2 -1
  37. package/stdlib/llm.js +2 -2
  38. package/dist/lib/preprocessors/typescriptPreprocessor.config.test.js +0 -327
  39. /package/dist/lib/{preprocessors/typescriptPreprocessor.config.test.d.ts → cli/doctor.test.d.ts} +0 -0
@@ -85,34 +85,6 @@ export declare class TypescriptPreprocessor {
85
85
  protected _insertAwaitPendingCalls(body: AgencyNode[], locationToVars: Record<string, string[]>, currentPath?: number[]): AgencyNode[];
86
86
  protected nodeHasBody(node: AgencyNode): node is FunctionDefinition | AgencyNode | IfElse | WhileLoop | MessageThread;
87
87
  protected _nodeUsesVariable(node: AgencyNode, varName: string): boolean;
88
- /**
89
- * Filter out nodes based on excludeNodeTypes config
90
- */
91
- protected filterExcludedNodeTypes(): void;
92
- /**
93
- * Recursively filter nodes by type, handling all node structures
94
- */
95
- protected filterNodesByType(nodes: AgencyNode[], excludeSet: Set<string>): AgencyNode[];
96
- /**
97
- * Filter out builtin function calls based on excludeBuiltinFunctions config
98
- */
99
- protected filterExcludedBuiltinFunctions(): void;
100
- /**
101
- * Recursively filter builtin function calls
102
- */
103
- protected filterBuiltinFunctionCalls(nodes: AgencyNode[], excludeSet: Set<string>): AgencyNode[];
104
- /**
105
- * Validate fetch calls against allowed/disallowed domains
106
- */
107
- protected validateFetchDomains(): void;
108
- /**
109
- * Validate a single fetch call's domain
110
- */
111
- protected _validateFetchCall(node: FunctionCall, effectiveAllowed: Set<string> | null, disallowedSet: Set<string>): void;
112
- /**
113
- * Extract domain from URL string
114
- */
115
- protected _extractDomain(url: string): string | null;
116
88
  /**
117
89
  * Validate that no async function calls appear inside loops.
118
90
  * Async calls inside loops can't be properly serialized for interrupt
@@ -272,9 +272,6 @@ export class TypescriptPreprocessor {
272
272
  injectSchemaArgsInProgram(this.program, this.functionDefinitions, this.importedFunctions);
273
273
  this.collectSkills();
274
274
  this.addAwaitPendingCalls();
275
- this.filterExcludedNodeTypes();
276
- this.filterExcludedBuiltinFunctions();
277
- this.validateFetchDomains();
278
275
  this.validateNoAsyncInLoops();
279
276
  this.resolveVariableScopes();
280
277
  return this.program;
@@ -729,248 +726,6 @@ export class TypescriptPreprocessor {
729
726
  }
730
727
  return false;
731
728
  }
732
- /**
733
- * Filter out nodes based on excludeNodeTypes config
734
- */
735
- filterExcludedNodeTypes() {
736
- if (!this.config.excludeNodeTypes ||
737
- this.config.excludeNodeTypes.length === 0) {
738
- return;
739
- }
740
- const excludeSet = new Set(this.config.excludeNodeTypes);
741
- this.program.nodes = this.filterNodesByType(this.program.nodes, excludeSet);
742
- }
743
- /**
744
- * Recursively filter nodes by type, handling all node structures
745
- */
746
- filterNodesByType(nodes, excludeSet) {
747
- const filteredNodes = [];
748
- for (const node of nodes) {
749
- // Skip nodes of excluded types
750
- if (excludeSet.has(node.type)) {
751
- continue;
752
- }
753
- // Recursively filter child nodes
754
- if (node.type === "function" || node.type === "graphNode") {
755
- node.body = this.filterNodesByType(node.body, excludeSet);
756
- }
757
- else if (node.type === "ifElse") {
758
- node.thenBody = this.filterNodesByType(node.thenBody, excludeSet);
759
- if (node.elseBody) {
760
- node.elseBody = this.filterNodesByType(node.elseBody, excludeSet);
761
- }
762
- }
763
- else if (node.type === "whileLoop") {
764
- node.body = this.filterNodesByType(node.body, excludeSet);
765
- }
766
- else if (node.type === "messageThread") {
767
- node.body = this.filterNodesByType(node.body, excludeSet);
768
- }
769
- else if (node.type === "handleBlock") {
770
- node.body = this.filterNodesByType(node.body, excludeSet);
771
- if (node.handler.kind === "inline") {
772
- node.handler.body = this.filterNodesByType(node.handler.body, excludeSet);
773
- }
774
- }
775
- else if (node.type === "withModifier") {
776
- const filtered = this.filterNodesByType([node.statement], excludeSet)[0];
777
- if (!filtered)
778
- continue;
779
- node.statement = filtered;
780
- }
781
- else if (node.type === "matchBlock") {
782
- // Case bodies are arrays of nodes, but this filter (filterNodesByType)
783
- // isn't wired up for match arms yet — mirrors pre-existing behavior.
784
- }
785
- else if (node.type === "assignment") {
786
- node.value = (this.filterNodesByType([node.value], excludeSet)[0] || node.value);
787
- }
788
- else if (node.type === "functionCall") {
789
- node.arguments = this.filterNodesByType(node.arguments, excludeSet);
790
- }
791
- else if (node.type === "agencyArray") {
792
- node.items = this.filterNodesByType(node.items, excludeSet);
793
- }
794
- else if (node.type === "agencyObject") {
795
- node.entries = node.entries.map((entry) => ({
796
- ...entry,
797
- value: (this.filterNodesByType([entry.value], excludeSet)[0] || entry.value),
798
- }));
799
- }
800
- filteredNodes.push(node);
801
- }
802
- return filteredNodes;
803
- }
804
- /**
805
- * Filter out builtin function calls based on excludeBuiltinFunctions config
806
- */
807
- filterExcludedBuiltinFunctions() {
808
- if (!this.config.excludeBuiltinFunctions ||
809
- this.config.excludeBuiltinFunctions.length === 0) {
810
- return;
811
- }
812
- const excludeSet = new Set(this.config.excludeBuiltinFunctions);
813
- this.program.nodes = this.filterBuiltinFunctionCalls(this.program.nodes, excludeSet);
814
- }
815
- /**
816
- * Recursively filter builtin function calls
817
- */
818
- filterBuiltinFunctionCalls(nodes, excludeSet) {
819
- const filteredNodes = [];
820
- for (const node of nodes) {
821
- // Skip excluded builtin function calls
822
- if (node.type === "functionCall" && excludeSet.has(node.functionName)) {
823
- continue;
824
- }
825
- // Skip assignments to excluded builtin function calls
826
- if (node.type === "assignment" &&
827
- node.value.type === "functionCall" &&
828
- excludeSet.has(node.value.functionName)) {
829
- continue;
830
- }
831
- // Recursively filter child nodes
832
- if (node.type === "function" || node.type === "graphNode") {
833
- node.body = this.filterBuiltinFunctionCalls(node.body, excludeSet);
834
- }
835
- else if (node.type === "ifElse") {
836
- node.thenBody = this.filterBuiltinFunctionCalls(node.thenBody, excludeSet);
837
- if (node.elseBody) {
838
- node.elseBody = this.filterBuiltinFunctionCalls(node.elseBody, excludeSet);
839
- }
840
- }
841
- else if (node.type === "whileLoop") {
842
- node.body = this.filterBuiltinFunctionCalls(node.body, excludeSet);
843
- }
844
- else if (node.type === "messageThread") {
845
- node.body = this.filterBuiltinFunctionCalls(node.body, excludeSet);
846
- }
847
- else if (node.type === "handleBlock") {
848
- node.body = this.filterBuiltinFunctionCalls(node.body, excludeSet);
849
- if (node.handler.kind === "inline") {
850
- node.handler.body = this.filterBuiltinFunctionCalls(node.handler.body, excludeSet);
851
- }
852
- }
853
- else if (node.type === "withModifier") {
854
- const filtered = this.filterBuiltinFunctionCalls([node.statement], excludeSet)[0];
855
- if (!filtered)
856
- continue;
857
- node.statement = filtered;
858
- }
859
- else if (node.type === "matchBlock") {
860
- node.cases = node.cases.map((caseItem) => {
861
- if (caseItem.type === "comment") {
862
- return caseItem;
863
- }
864
- if (caseItem.type === "newLine") {
865
- return caseItem;
866
- }
867
- return {
868
- ...caseItem,
869
- body: this.filterBuiltinFunctionCalls(caseItem.body, excludeSet),
870
- };
871
- });
872
- }
873
- else if (node.type === "functionCall") {
874
- // Filter arguments that are function calls
875
- node.arguments = this.filterBuiltinFunctionCalls(node.arguments, excludeSet);
876
- }
877
- else if (node.type === "agencyArray") {
878
- node.items = this.filterBuiltinFunctionCalls(node.items, excludeSet);
879
- }
880
- else if (node.type === "agencyObject") {
881
- node.entries = node.entries.map((entry) => {
882
- const filteredValues = this.filterBuiltinFunctionCalls([entry.value], excludeSet);
883
- return {
884
- ...entry,
885
- value: filteredValues[0] || entry.value,
886
- };
887
- });
888
- }
889
- filteredNodes.push(node);
890
- }
891
- return filteredNodes;
892
- }
893
- /**
894
- * Validate fetch calls against allowed/disallowed domains
895
- */
896
- validateFetchDomains() {
897
- const hasAllowed = this.config.allowedFetchDomains &&
898
- this.config.allowedFetchDomains.length > 0;
899
- const hasDisallowed = this.config.disallowedFetchDomains &&
900
- this.config.disallowedFetchDomains.length > 0;
901
- if (!hasAllowed && !hasDisallowed) {
902
- return; // No domain restrictions
903
- }
904
- // Compute the effective allowed domains
905
- let effectiveAllowed = null;
906
- if (hasAllowed) {
907
- effectiveAllowed = new Set(this.config.allowedFetchDomains);
908
- // If both allowed and disallowed are set, remove disallowed from allowed
909
- if (hasDisallowed) {
910
- for (const domain of this.config.disallowedFetchDomains) {
911
- effectiveAllowed.delete(domain);
912
- }
913
- }
914
- }
915
- const disallowedSet = hasDisallowed
916
- ? new Set(this.config.disallowedFetchDomains)
917
- : new Set();
918
- // Walk through all nodes and validate fetch calls
919
- for (const { node } of walkNodesArray(this.program.nodes)) {
920
- if (node.type === "functionCall" &&
921
- (node.functionName === "fetch" ||
922
- node.functionName === "fetchJSON" ||
923
- node.functionName === "fetchJson")) {
924
- this._validateFetchCall(node, effectiveAllowed, disallowedSet);
925
- }
926
- }
927
- }
928
- /**
929
- * Validate a single fetch call's domain
930
- */
931
- _validateFetchCall(node, effectiveAllowed, disallowedSet) {
932
- // Get the URL argument (first argument to fetch)
933
- if (node.arguments.length === 0) {
934
- return; // No URL provided, let it fail at runtime
935
- }
936
- const urlArg = node.arguments[0];
937
- let url = null;
938
- // Extract URL if it's a string literal
939
- if (urlArg.type === "string") {
940
- // Reconstruct the URL from segments
941
- url = urlArg.segments
942
- .map((seg) => (seg.type === "text" ? seg.value : ""))
943
- .join("");
944
- }
945
- if (!url) {
946
- return; // Can't validate variable URLs at compile time
947
- }
948
- // Extract domain from URL
949
- const domain = this._extractDomain(url);
950
- if (!domain) {
951
- return; // Can't extract domain, skip validation
952
- }
953
- // Check if domain is allowed
954
- if (effectiveAllowed !== null && !effectiveAllowed.has(domain)) {
955
- throw new Error(`Fetch to domain "${domain}" is not allowed. Allowed domains: ${Array.from(effectiveAllowed).join(", ")}`);
956
- }
957
- // Check if domain is disallowed
958
- if (disallowedSet.has(domain)) {
959
- throw new Error(`Fetch to domain "${domain}" is explicitly disallowed.`);
960
- }
961
- }
962
- /**
963
- * Extract domain from URL string
964
- */
965
- _extractDomain(url) {
966
- try {
967
- const urlObj = new URL(url);
968
- return urlObj.hostname;
969
- }
970
- catch {
971
- return null; // Invalid URL
972
- }
973
- }
974
729
  /**
975
730
  * Validate that no async function calls appear inside loops.
976
731
  * Async calls inside loops can't be properly serialized for interrupt
@@ -20,22 +20,23 @@ export type RuntimeContextConstructorArgs = {
20
20
  export declare function setRuntimeConfigOverrides(overrides: Partial<AgencyConfig> | undefined): void;
21
21
  export declare function getRuntimeConfigOverrides(): Partial<AgencyConfig> | undefined;
22
22
  /**
23
- * Apply runtime config overrides (set per-subprocess via
24
- * `setRuntimeConfigOverrides`) to the args used to construct a
25
- * `RuntimeContext`. `log.*` fields and the top-level `observability` flag flow
26
- * into the statelog config; `client.providerModules` and `maxCallDepth` are
27
- * forwarded explicitly (see below). Other `AgencyConfig` fields are ignored
28
- * because the runtime has its own pathways for them. If a new statelog-relevant
29
- * override field is added, it will be picked up automatically by the shallow
30
- * merge below.
23
+ * Apply runtime config overrides a `Partial<AgencyConfig>` — onto the args
24
+ * used to construct a `RuntimeContext`. This is THE single runtime merge; it
25
+ * serves two transports:
26
+ * subprocess IPC: the parent forwards `configOverrides` in the spawn
27
+ * message (`setRuntimeConfigOverrides` sets the active value).
28
+ * bundled agents / packed bundles: `AGENCY_CONFIG_OVERRIDES` in the env
29
+ * (`config.ts` `readConfigOverrides`), passed explicitly by the caller.
31
30
  *
32
- * `client.providerModules` is honored: `_run` forwards the parent's
33
- * (absolutized) provider-module paths here so a subprocess loads the same
34
- * custom/local providers the parent has, regardless of how the child was
35
- * compiled. They are merged with any baked-in `providerModules` on `args`;
36
- * `loadProviderModules` de-duplicates by resolved path at load time.
37
- *
38
- * `maxCallDepth` is honored so a subprocess inherits the parent's runaway-
39
- * recursion ceiling rather than falling back to the default.
31
+ * Fields honored (others are ignored — the runtime has its own pathways):
32
+ * `log.*` + `observability` statelogConfig
33
+ * `traceFile` / `traceDir` traceConfig (the `trace` boolean is only a
34
+ * marker set alongside them by applyCliFlags; the file/dir drive output).
35
+ * An override always wins over the baked traceConfig: a supplied `traceDir`
36
+ * clears any baked `traceFile` (which `resolveTraceFilePath` would otherwise
37
+ * prefer), so a per-run `--trace` dir actually takes effect.
38
+ * `client.providerModules` merged with baked modules (subprocess loads
39
+ * the same custom/local providers; de-duped at load time).
40
+ * • `maxCallDepth` → inherit the parent's runaway-recursion ceiling.
40
41
  */
41
42
  export declare function applyRuntimeConfigOverridesToContextArgs(args: RuntimeContextConstructorArgs, overrides?: Partial<AgencyConfig> | undefined): RuntimeContextConstructorArgs;
@@ -6,23 +6,24 @@ export function getRuntimeConfigOverrides() {
6
6
  return activeRuntimeConfigOverrides;
7
7
  }
8
8
  /**
9
- * Apply runtime config overrides (set per-subprocess via
10
- * `setRuntimeConfigOverrides`) to the args used to construct a
11
- * `RuntimeContext`. `log.*` fields and the top-level `observability` flag flow
12
- * into the statelog config; `client.providerModules` and `maxCallDepth` are
13
- * forwarded explicitly (see below). Other `AgencyConfig` fields are ignored
14
- * because the runtime has its own pathways for them. If a new statelog-relevant
15
- * override field is added, it will be picked up automatically by the shallow
16
- * merge below.
9
+ * Apply runtime config overrides a `Partial<AgencyConfig>` — onto the args
10
+ * used to construct a `RuntimeContext`. This is THE single runtime merge; it
11
+ * serves two transports:
12
+ * subprocess IPC: the parent forwards `configOverrides` in the spawn
13
+ * message (`setRuntimeConfigOverrides` sets the active value).
14
+ * bundled agents / packed bundles: `AGENCY_CONFIG_OVERRIDES` in the env
15
+ * (`config.ts` `readConfigOverrides`), passed explicitly by the caller.
17
16
  *
18
- * `client.providerModules` is honored: `_run` forwards the parent's
19
- * (absolutized) provider-module paths here so a subprocess loads the same
20
- * custom/local providers the parent has, regardless of how the child was
21
- * compiled. They are merged with any baked-in `providerModules` on `args`;
22
- * `loadProviderModules` de-duplicates by resolved path at load time.
23
- *
24
- * `maxCallDepth` is honored so a subprocess inherits the parent's runaway-
25
- * recursion ceiling rather than falling back to the default.
17
+ * Fields honored (others are ignored — the runtime has its own pathways):
18
+ * `log.*` + `observability` statelogConfig
19
+ * `traceFile` / `traceDir` traceConfig (the `trace` boolean is only a
20
+ * marker set alongside them by applyCliFlags; the file/dir drive output).
21
+ * An override always wins over the baked traceConfig: a supplied `traceDir`
22
+ * clears any baked `traceFile` (which `resolveTraceFilePath` would otherwise
23
+ * prefer), so a per-run `--trace` dir actually takes effect.
24
+ * `client.providerModules` merged with baked modules (subprocess loads
25
+ * the same custom/local providers; de-duped at load time).
26
+ * • `maxCallDepth` → inherit the parent's runaway-recursion ceiling.
26
27
  */
27
28
  export function applyRuntimeConfigOverridesToContextArgs(args, overrides = activeRuntimeConfigOverrides) {
28
29
  if (!overrides)
@@ -37,6 +38,16 @@ export function applyRuntimeConfigOverridesToContextArgs(args, overrides = activ
37
38
  ...args,
38
39
  statelogConfig: { ...args.statelogConfig, ...statelogOverrides },
39
40
  };
41
+ if (overrides.traceFile || overrides.traceDir) {
42
+ merged.traceConfig = {
43
+ ...(args.traceConfig ?? {}),
44
+ // Override wins over baked. An explicit file sets traceFile; a dir-only
45
+ // override clears any baked traceFile so the dir is honored (see above).
46
+ ...(overrides.traceFile
47
+ ? { traceFile: overrides.traceFile }
48
+ : { traceFile: undefined, traceDir: overrides.traceDir }),
49
+ };
50
+ }
40
51
  const overrideModules = overrides.client?.providerModules;
41
52
  if (overrideModules && overrideModules.length > 0) {
42
53
  merged.providerModules = [
@@ -1,5 +1,8 @@
1
- import { describe, expect, it } from "vitest";
2
- import { applyRuntimeConfigOverridesToContextArgs } from "./configOverrides.js";
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { applyRuntimeConfigOverridesToContextArgs, setRuntimeConfigOverrides, } from "./configOverrides.js";
3
+ import { CONFIG_OVERRIDES_ENV, serializeConfigOverrides, } from "../config.js";
4
+ import { RuntimeContext } from "./state/context.js";
5
+ import { agentConfigOverride } from "../cli/runBundledAgent.js";
3
6
  describe("runtime config overrides", () => {
4
7
  it("applies eval run statelog overrides without replacing existing fields", () => {
5
8
  const result = applyRuntimeConfigOverridesToContextArgs({
@@ -57,3 +60,114 @@ describe("runtime config overrides", () => {
57
60
  expect(result.providerModules).toEqual(["/baked/a.mjs"]);
58
61
  });
59
62
  });
63
+ describe("applyRuntimeConfigOverridesToContextArgs — trace overrides", () => {
64
+ const baseArgs = () => ({
65
+ statelogConfig: {
66
+ host: "https://kept",
67
+ apiKey: "",
68
+ projectId: "",
69
+ debugMode: true,
70
+ observability: false,
71
+ },
72
+ smoltalkDefaults: {},
73
+ dirname: "/project",
74
+ traceConfig: { program: "agent" },
75
+ });
76
+ it("folds an override traceFile and preserves traceConfig.program", () => {
77
+ const r = applyRuntimeConfigOverridesToContextArgs(baseArgs(), {
78
+ trace: true,
79
+ traceFile: "/t.agencytrace",
80
+ });
81
+ expect(r.traceConfig?.traceFile).toBe("/t.agencytrace");
82
+ expect(r.traceConfig?.program).toBe("agent");
83
+ });
84
+ it("folds an override traceDir", () => {
85
+ const r = applyRuntimeConfigOverridesToContextArgs(baseArgs(), {
86
+ trace: true,
87
+ traceDir: "/traces",
88
+ });
89
+ expect(r.traceConfig?.traceDir).toBe("/traces");
90
+ });
91
+ it("an override traceDir clears a baked traceFile so the dir takes effect", () => {
92
+ const baked = { ...baseArgs(), traceConfig: { program: "agent", traceFile: "baked.trace" } };
93
+ const r = applyRuntimeConfigOverridesToContextArgs(baked, {
94
+ trace: true,
95
+ traceDir: "/x",
96
+ });
97
+ expect(r.traceConfig?.traceFile).toBeUndefined();
98
+ expect(r.traceConfig?.traceDir).toBe("/x");
99
+ });
100
+ it("applies trace + statelog overrides together, keeping statelog siblings", () => {
101
+ const r = applyRuntimeConfigOverridesToContextArgs(baseArgs(), {
102
+ trace: true,
103
+ traceFile: "/t",
104
+ observability: true,
105
+ log: { logFile: "/l" },
106
+ });
107
+ expect(r.traceConfig?.traceFile).toBe("/t");
108
+ expect(r.statelogConfig.logFile).toBe("/l");
109
+ expect(r.statelogConfig.observability).toBe(true);
110
+ expect(r.statelogConfig.host).toBe("https://kept");
111
+ });
112
+ it("is a no-op when overrides are empty/undefined", () => {
113
+ expect(applyRuntimeConfigOverridesToContextArgs(baseArgs(), {}).traceConfig?.traceFile).toBeUndefined();
114
+ expect(applyRuntimeConfigOverridesToContextArgs(baseArgs(), undefined).traceConfig?.program).toBe("agent");
115
+ });
116
+ });
117
+ describe("RuntimeContext honors AGENCY_CONFIG_OVERRIDES at construction", () => {
118
+ const saved = { ...process.env };
119
+ afterEach(() => {
120
+ process.env = { ...saved };
121
+ });
122
+ it("reads the env override into statelogConfig", () => {
123
+ process.env[CONFIG_OVERRIDES_ENV] = serializeConfigOverrides({
124
+ observability: true,
125
+ log: { logFile: "/tmp/wired.jsonl" },
126
+ });
127
+ const ctx = new RuntimeContext({
128
+ statelogConfig: { host: "", apiKey: "", projectId: "", debugMode: false, observability: false },
129
+ smoltalkDefaults: {},
130
+ dirname: "/project",
131
+ });
132
+ const sc = ctx.statelogConfig;
133
+ expect(sc.logFile).toBe("/tmp/wired.jsonl");
134
+ expect(sc.observability).toBe(true);
135
+ });
136
+ it("still applies the env override when an IPC override is also present (both transports layer)", () => {
137
+ process.env[CONFIG_OVERRIDES_ENV] = serializeConfigOverrides({
138
+ observability: true,
139
+ log: { logFile: "/tmp/tree.jsonl" },
140
+ });
141
+ // A subprocess launched with explicit IPC overrides (e.g. an eval run) must
142
+ // NOT drop out of the env-based statelog tree.
143
+ setRuntimeConfigOverrides({ maxCallDepth: 7 });
144
+ try {
145
+ const ctx = new RuntimeContext({
146
+ statelogConfig: { host: "", apiKey: "", projectId: "", debugMode: false, observability: false },
147
+ smoltalkDefaults: {},
148
+ dirname: "/project",
149
+ });
150
+ const sc = ctx.statelogConfig;
151
+ expect(sc.logFile).toBe("/tmp/tree.jsonl");
152
+ expect(sc.observability).toBe(true);
153
+ }
154
+ finally {
155
+ setRuntimeConfigOverrides(undefined);
156
+ }
157
+ });
158
+ });
159
+ describe("writer→reader override contract", () => {
160
+ it("agentConfigOverride output applies cleanly through the runtime merge", () => {
161
+ const base = {
162
+ statelogConfig: { host: "", apiKey: "", projectId: "", debugMode: false, observability: false },
163
+ smoltalkDefaults: {},
164
+ dirname: "/p",
165
+ traceConfig: { program: "agent" },
166
+ };
167
+ // The exact object runBundledAgent serializes into AGENCY_CONFIG_OVERRIDES.
168
+ const overrides = agentConfigOverride(["--trace", "t.trace", "--log-file", "l.jsonl"]);
169
+ const r = applyRuntimeConfigOverridesToContextArgs(base, overrides);
170
+ expect(r.traceConfig?.traceFile).toBe("t.trace");
171
+ expect(r.statelogConfig.logFile).toBe("l.jsonl");
172
+ });
173
+ });
@@ -536,7 +536,11 @@ export async function runPrompt(args) {
536
536
  // `smoltalkDefaults` and the per-call options, so precedence is
537
537
  // baked agency.json < stack defaults < per-call `llm({...})`.
538
538
  const stackDefaults = stateStack?.other?.llmDefaults ?? {};
539
- const { maxToolResultChars: stackMaxToolResultChars, ...stackSmolDefaults } = stackDefaults;
539
+ const { maxToolResultChars: stackMaxToolResultChars, maxToolCallRounds: stackMaxToolCallRounds, ...stackSmolDefaults } = stackDefaults;
540
+ // maxToolCallRounds precedence: a branch default (setLlmOptions) overrides the
541
+ // baked per-call value (agency.json → codegen literal, default 10). Kept out
542
+ // of stackSmolDefaults above — it isn't a smoltalk config field.
543
+ const effectiveMaxToolCallRounds = stackMaxToolCallRounds ?? maxToolCallRounds;
540
544
  const clientConfig = ctx.getSmoltalkConfig({
541
545
  ...stackSmolDefaults,
542
546
  ...restClientConfig,
@@ -882,9 +886,9 @@ export async function runPrompt(args) {
882
886
  });
883
887
  return;
884
888
  }
885
- if (self.toolCallRound >= maxToolCallRounds) {
889
+ if (self.toolCallRound >= effectiveMaxToolCallRounds) {
886
890
  await b.step(`round.${round}.tool.${callSlug}.tooManyRounds`, async () => {
887
- messages.push(smoltalk.toolMessage(`Error: Maximum number of tool call rounds (${maxToolCallRounds}) exceeded. This tool call will not be executed.`, { tool_call_id: toolCall.id, name: toolCall.name }));
891
+ messages.push(smoltalk.toolMessage(`Error: Maximum number of tool call rounds (${effectiveMaxToolCallRounds}) exceeded. This tool call will not be executed.`, { tool_call_id: toolCall.id, name: toolCall.name }));
888
892
  });
889
893
  return;
890
894
  }
@@ -14,7 +14,8 @@ import { getOrCreateStore } from "../memory/registry.js";
14
14
  import { GlobalStore } from "../state/globalStore.js";
15
15
  import { StateStack } from "../state/stateStack.js";
16
16
  import { TraceWriter } from "../trace/traceWriter.js";
17
- import { applyRuntimeConfigOverridesToContextArgs } from "../configOverrides.js";
17
+ import { applyRuntimeConfigOverridesToContextArgs, getRuntimeConfigOverrides, } from "../configOverrides.js";
18
+ import { readConfigOverrides } from "../../config.js";
18
19
  import { CheckpointStore, RESULT_ENTRY_LABEL } from "./checkpointStore.js";
19
20
  import { PendingPromiseStore } from "./pendingPromiseStore.js";
20
21
  /**
@@ -161,7 +162,13 @@ export class RuntimeContext {
161
162
  jsonMemoryConfig;
162
163
  memoryManagerCache = {};
163
164
  constructor(args) {
164
- args = applyRuntimeConfigOverridesToContextArgs(args);
165
+ // One runtime merge, applied for BOTH transports so a subprocess launched
166
+ // with explicit IPC overrides still inherits the env override (e.g. a
167
+ // tree-wide --log-file). Env first, then IPC on top: IPC (explicit,
168
+ // per-child) wins per field, env fills the rest. Sequential application
169
+ // also layers nested `log`/`trace` objects rather than clobbering them.
170
+ args = applyRuntimeConfigOverridesToContextArgs(args, readConfigOverrides());
171
+ args = applyRuntimeConfigOverridesToContextArgs(args, getRuntimeConfigOverrides());
165
172
  const statelogConfig = {
166
173
  ...args.statelogConfig,
167
174
  traceId: args.statelogConfig.traceId || nanoid(),
@@ -17,6 +17,7 @@ export type LlmDefaults = RetryConfig & {
17
17
  reasoningEffort?: "low" | "medium" | "high";
18
18
  maxTokens?: number;
19
19
  maxToolResultChars?: number;
20
+ maxToolCallRounds?: number;
20
21
  };
21
22
  /**
22
23
  * Merge `opts` into the ACTIVE branch stack's LLM defaults
@@ -1 +1 @@
1
- export declare const VERSION = "0.7.1";
1
+ export declare const VERSION = "0.7.2";
@@ -1 +1 @@
1
- export const VERSION = "0.7.1";
1
+ export const VERSION = "0.7.2";
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ export declare function parsePositiveInt(value: string): number;
4
+ export declare function parseNonNegativeInt(value: string): number;
3
5
  type CliDependencies = {
4
6
  loadLspStartServer?: () => Promise<() => void>;
5
7
  loadMcpStartServer?: () => Promise<() => void>;