Skip to content

Commit ef03ceb

Browse files
committed
feat: implement patchGlobals method for AxCodeSession and update related functionality
1 parent 8b8e628 commit ef03ceb

File tree

9 files changed

+616
-215
lines changed

9 files changed

+616
-215
lines changed

docs/AXAGENT.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ const analyzer = agent('query:string -> answer:string', {
731731
});
732732
```
733733

734-
Updates from this callback are merged into current inputs (unknown keys are ignored), then synchronized into runtime `inputs.<field>` and existing non-colliding top-level aliases before code execution.
734+
Updates from this callback are merged into current inputs (unknown keys are ignored), then synchronized into runtime `inputs.<field>` and existing non-colliding top-level aliases via `AxCodeSession.patchGlobals(...)` before code execution. This host-side sync does not run through the Actor's `execute(code)` path.
735735

736736
### Actor/Responder Forward Options
737737

@@ -922,11 +922,37 @@ class MyBrowserInterpreter implements AxCodeRuntime {
922922
}
923923

924924
createSession(globals?: Record<string, unknown>): AxCodeSession {
925+
const scope = { ...globals };
926+
const isPlainObject = (
927+
value: unknown
928+
): value is Record<string, unknown> => {
929+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
930+
return false;
931+
}
932+
const proto = Object.getPrototypeOf(value);
933+
return proto === Object.prototype || proto === null;
934+
};
935+
925936
// Set up your execution environment with globals
926937
return {
927938
async execute(code: string) {
928939
// Execute code and return result
929940
},
941+
async patchGlobals(nextGlobals: Record<string, unknown>) {
942+
for (const [key, value] of Object.entries(nextGlobals)) {
943+
const current = scope[key];
944+
if (isPlainObject(current) && isPlainObject(value)) {
945+
for (const existingKey of Object.keys(current)) {
946+
if (!(existingKey in value)) {
947+
delete current[existingKey];
948+
}
949+
}
950+
Object.assign(current, value);
951+
continue;
952+
}
953+
scope[key] = value;
954+
}
955+
},
930956
close() {
931957
// Clean up resources
932958
},
@@ -935,6 +961,8 @@ class MyBrowserInterpreter implements AxCodeRuntime {
935961
}
936962
```
937963

964+
When patching object-valued globals such as `inputs`, reconcile the existing object in place instead of blindly replacing the reference. That keeps previously saved references in the runtime session aligned with later host-side updates.
965+
938966
The `globals` object passed to `createSession` includes:
939967
- All context field values (by field name)
940968
- `llmQuery` function (supports both single and batched queries)
@@ -1020,7 +1048,14 @@ interface AxCodeRuntime {
10201048

10211049
```typescript
10221050
interface AxCodeSession {
1023-
execute(code: string, options?: { signal?: AbortSignal });
1051+
execute(
1052+
code: string,
1053+
options?: { signal?: AbortSignal; reservedNames?: readonly string[] }
1054+
): Promise<unknown>;
1055+
patchGlobals(
1056+
globals: Record<string, unknown>,
1057+
options?: { signal?: AbortSignal }
1058+
): Promise<void>;
10241059
close(): void;
10251060
}
10261061
```

src/ax/ai/google-gemini/agent-flow.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { describe, expect, it } from 'vitest';
2-
3-
import { AxMockAIService } from '../mock/api.js';
42
import { agent, s } from '../../index.js';
53
import type { AxCodeRuntime } from '../../prompts/rlm.js';
4+
import { AxMockAIService } from '../mock/api.js';
65

76
const makeModelUsage = () => ({
87
ai: 'mock',
@@ -119,6 +118,7 @@ describe('Agent Split Architecture Flow', () => {
119118
}
120119
return `executed: ${code}`;
121120
},
121+
patchGlobals: async () => {},
122122
close: () => {},
123123
};
124124
},

src/ax/funcs/jsRuntime.integration.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,4 +698,94 @@ describe('AxJSRuntime integration', () => {
698698
session.close();
699699
}
700700
});
701+
702+
it('patchGlobals overwrites scalars and reconciles plain objects in place', async () => {
703+
const runtime = new AxJSRuntime({ outputMode: 'return' });
704+
const session = runtime.createSession({
705+
query: 'initial',
706+
tags: ['old'],
707+
inputs: { query: 'initial', stale: 'old' },
708+
});
709+
710+
try {
711+
await session.execute('globalThis.savedInputs = inputs');
712+
await session.patchGlobals({
713+
query: 'updated',
714+
tags: ['fresh'],
715+
note: undefined,
716+
inputs: { query: 'updated' },
717+
helper: {
718+
describe: async () => 'patched helper',
719+
},
720+
});
721+
722+
const result = await session.execute(`
723+
({
724+
query,
725+
tags,
726+
note,
727+
savedInputsSame: globalThis.savedInputs === inputs,
728+
savedInputsQuery: globalThis.savedInputs.query,
729+
inputsKeys: Object.keys(inputs).sort(),
730+
helperResult: await helper.describe()
731+
})
732+
`);
733+
734+
expect(result).toEqual({
735+
query: 'updated',
736+
tags: ['fresh'],
737+
note: undefined,
738+
savedInputsSame: true,
739+
savedInputsQuery: 'updated',
740+
inputsKeys: ['query'],
741+
helperResult: 'patched helper',
742+
});
743+
} finally {
744+
session.close();
745+
}
746+
});
747+
748+
it('patchGlobals survives worker reset and recreation', async () => {
749+
const runtime = new AxJSRuntime({ outputMode: 'return', timeout: 100 });
750+
const session = runtime.createSession({
751+
query: 'initial',
752+
inputs: { query: 'initial' },
753+
});
754+
755+
try {
756+
await session.patchGlobals({
757+
query: 'updated',
758+
inputs: { query: 'updated' },
759+
});
760+
761+
await expect(session.execute('while(true){}')).rejects.toThrow(
762+
'Execution timed out'
763+
);
764+
765+
const result = await session.execute(
766+
'({ query, inputQuery: inputs.query })'
767+
);
768+
expect(result).toEqual({ query: 'updated', inputQuery: 'updated' });
769+
} finally {
770+
session.close();
771+
}
772+
});
773+
774+
it('patchGlobals respects abort signals', async () => {
775+
const runtime = new AxJSRuntime({ outputMode: 'return' });
776+
const session = runtime.createSession();
777+
const controller = new AbortController();
778+
controller.abort('stop patching');
779+
780+
try {
781+
await expect(
782+
session.patchGlobals(
783+
{ query: 'updated' },
784+
{ signal: controller.signal }
785+
)
786+
).rejects.toThrow('Aborted: stop patching');
787+
} finally {
788+
session.close();
789+
}
790+
});
701791
});

0 commit comments

Comments
 (0)