Skip to content

Commit 7b670f8

Browse files
committed
Support running string of generator code in quick-js sandbox
1 parent cad99e8 commit 7b670f8

File tree

7 files changed

+454
-14
lines changed

7 files changed

+454
-14
lines changed

backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
"express": "4.19.2",
3939
"gpt-tokenizer": "2.8.1",
4040
"ignore": "5.3.2",
41+
"@jitl/quickjs-wasmfile-release-sync": "0.31.0",
42+
"quickjs-emscripten-core": "0.31.0",
4143
"lodash": "*",
4244
"openai": "^4.78.1",
4345
"pino": "9.4.0",
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2+
import {
3+
runProgrammaticStep,
4+
clearAgentGeneratorCache,
5+
} from '../run-programmatic-step'
6+
import { AgentState } from '@codebuff/common/types/session-state'
7+
import { AgentTemplate } from '../templates/types'
8+
import { WebSocket } from 'ws'
9+
import { mockFileContext, MockWebSocket } from './test-utils'
10+
11+
describe('QuickJS Sandbox Generator', () => {
12+
let mockAgentState: AgentState
13+
let mockParams: any
14+
let mockTemplate: AgentTemplate
15+
16+
beforeEach(() => {
17+
clearAgentGeneratorCache()
18+
19+
// Reuse common test data structure
20+
mockAgentState = {
21+
agentId: 'test-agent-123',
22+
agentType: 'test-vm-agent',
23+
messageHistory: [],
24+
report: {},
25+
agentContext: {},
26+
subagents: [],
27+
stepsRemaining: 10,
28+
}
29+
30+
// Base template structure - will be customized per test
31+
mockTemplate = {
32+
id: 'test-vm-agent',
33+
name: 'Test VM Agent',
34+
purpose: 'Test VM isolation',
35+
model: 'anthropic/claude-4-sonnet-20250522',
36+
outputMode: 'report',
37+
includeMessageHistory: false,
38+
toolNames: ['update_report'],
39+
spawnableAgents: [],
40+
promptSchema: {},
41+
systemPrompt: '',
42+
userInputPrompt: '',
43+
agentStepPrompt: '',
44+
initialAssistantMessage: '',
45+
initialAssistantPrefix: '',
46+
stepAssistantMessage: '',
47+
stepAssistantPrefix: '',
48+
handleStep: '', // Will be set per test
49+
}
50+
51+
// Common params structure
52+
mockParams = {
53+
template: mockTemplate,
54+
prompt: 'Test prompt',
55+
params: { testParam: 'value' },
56+
userId: 'test-user',
57+
userInputId: 'test-input',
58+
clientSessionId: 'test-session',
59+
fingerprintId: 'test-fingerprint',
60+
onResponseChunk: () => {},
61+
agentType: 'test-vm-agent',
62+
fileContext: mockFileContext,
63+
assistantMessage: undefined,
64+
assistantPrefix: undefined,
65+
ws: new MockWebSocket() as unknown as WebSocket,
66+
}
67+
})
68+
69+
afterEach(() => {
70+
clearAgentGeneratorCache()
71+
})
72+
73+
test('should execute string-based generator in QuickJS sandbox', async () => {
74+
// Customize template for this test
75+
mockTemplate.handleStep = `
76+
function* ({ agentState, prompt, params }) {
77+
yield {
78+
toolName: 'update_report',
79+
args: {
80+
json_update: {
81+
message: 'Hello from QuickJS sandbox!',
82+
prompt: prompt,
83+
agentId: agentState.agentId
84+
}
85+
}
86+
}
87+
}
88+
`
89+
mockParams.template = mockTemplate
90+
91+
const result = await runProgrammaticStep(mockAgentState, mockParams)
92+
93+
expect(result.agentState.report).toEqual({
94+
message: 'Hello from QuickJS sandbox!',
95+
prompt: 'Test prompt',
96+
agentId: 'test-agent-123',
97+
})
98+
expect(result.endTurn).toBe(true)
99+
})
100+
101+
test('should handle QuickJS sandbox errors gracefully', async () => {
102+
// Customize for error test
103+
mockTemplate.id = 'test-vm-agent-error'
104+
mockTemplate.name = 'Test VM Agent Error'
105+
mockTemplate.purpose = 'Test QuickJS error handling'
106+
mockTemplate.toolNames = []
107+
mockTemplate.handleStep = `
108+
function* ({ agentState, prompt, params }) {
109+
throw new Error('QuickJS error test')
110+
}
111+
`
112+
113+
mockAgentState.agentId = 'test-agent-error-123'
114+
mockAgentState.agentType = 'test-vm-agent-error'
115+
116+
mockParams.template = mockTemplate
117+
mockParams.agentType = 'test-vm-agent-error'
118+
mockParams.params = {}
119+
120+
const result = await runProgrammaticStep(mockAgentState, mockParams)
121+
122+
expect(result.endTurn).toBe(true)
123+
expect(result.agentState.report.error).toContain(
124+
'Error executing programmatic agent'
125+
)
126+
})
127+
})

backend/src/__tests__/test-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const mockFileContext: ProjectFileContext = {
1313
fileTree: [],
1414
fileTokenScores: {},
1515
knowledgeFiles: {},
16+
userKnowledgeFiles: {},
1617
agentTemplates: {},
1718
gitChanges: {
1819
status: '',

backend/src/run-programmatic-step.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import { CodebuffToolCall } from './tools/constants'
1010
import { executeToolCall } from './tools/tool-executor'
1111
import { logger } from './util/logger'
1212
import { getRequestContext } from './websockets/request-context'
13+
import { SandboxManager } from './util/quickjs-sandbox'
14+
15+
// Global sandbox manager for QuickJS contexts
16+
const sandboxManager = new SandboxManager()
1317

1418
// Maintains generator state for all agents. Generator state can't be serialized, so we store it in memory.
1519
const agentIdToGenerator: Record<
@@ -22,6 +26,8 @@ export function clearAgentGeneratorCache() {
2226
for (const key in agentIdToGenerator) {
2327
delete agentIdToGenerator[key]
2428
}
29+
// Clean up QuickJS sandboxes
30+
sandboxManager.dispose()
2531
}
2632

2733
// Function to handle programmatic agents
@@ -53,6 +59,9 @@ export async function runProgrammaticStep(
5359
fingerprintId,
5460
fileContext,
5561
} = params
62+
if (!template.handleStep) {
63+
throw new Error('No step handler found for agent template ' + template.id)
64+
}
5665

5766
logger.info(
5867
{
@@ -64,18 +73,34 @@ export async function runProgrammaticStep(
6473
'Running programmatic step'
6574
)
6675

76+
// Run with either a generator or a sandbox.
6777
let generator = agentIdToGenerator[agentState.agentId]
68-
if (!generator) {
69-
if (!template.handleStep) {
70-
throw new Error('No step handler found for agent template ' + template.id)
78+
let sandbox = sandboxManager.getSandbox(agentState.agentId)
79+
80+
// Check if we need to initialize a generator (either native or QuickJS-based)
81+
if (!generator && !sandbox) {
82+
if (typeof template.handleStep === 'string') {
83+
// Initialize QuickJS sandbox for string-based generator
84+
sandbox = await sandboxManager.getOrCreateSandbox(
85+
agentState.agentId,
86+
template.handleStep,
87+
{
88+
agentState,
89+
prompt: params.prompt,
90+
params: params.params,
91+
}
92+
)
93+
} else {
94+
// Initialize native generator
95+
generator = template.handleStep({
96+
agentState,
97+
prompt: params.prompt,
98+
params: params.params,
99+
})
100+
agentIdToGenerator[agentState.agentId] = generator
71101
}
72-
generator = template.handleStep({
73-
agentState,
74-
prompt: params.prompt,
75-
params: params.params,
76-
})
77-
agentIdToGenerator[agentState.agentId] = generator
78102
}
103+
79104
if (generator === 'STEP_ALL') {
80105
return { agentState, endTurn: false }
81106
}
@@ -105,10 +130,16 @@ export async function runProgrammaticStep(
105130
try {
106131
// Execute tools synchronously as the generator yields them
107132
do {
108-
let result = generator.next({
109-
agentState: { ...state.agentState },
110-
toolResult,
111-
})
133+
const result = sandbox
134+
? await sandbox.executeStep({
135+
agentState: { ...state.agentState },
136+
toolResult,
137+
})
138+
: generator!.next({
139+
agentState: { ...state.agentState },
140+
toolResult,
141+
})
142+
112143
if (result.done) {
113144
endTurn = true
114145
break
@@ -185,5 +216,10 @@ export async function runProgrammaticStep(
185216
agentState: state.agentState,
186217
endTurn: true,
187218
}
219+
} finally {
220+
// Clean up QuickJS sandbox if execution is complete
221+
if (endTurn && sandbox) {
222+
sandboxManager.removeSandbox(agentState.agentId)
223+
}
188224
}
189225
}

backend/src/templates/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export type AgentTemplate<
3838
userInputPrompt: string
3939
agentStepPrompt: string
4040

41-
handleStep?: StepHandler<P, T>
41+
handleStep?: StepHandler<P, T> | string // Function or string of the generator code for running in a sandbox
4242
}
4343

4444
export type StepGenerator = Generator<

0 commit comments

Comments
 (0)