-
Notifications
You must be signed in to change notification settings - Fork 0
feat(runtime): ctx.team client for sandboxed agent teams #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
khaliqgant
wants to merge
2
commits into
main
Choose a base branch
from
feat/ctx-team-v1
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,359 @@ | ||
| import test from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import type { PersonaSpec } from '@agentworkforce/persona-kit'; | ||
| import { createCloudRuntimeDefaults } from './cloud-defaults.js'; | ||
|
|
||
| const persona: PersonaSpec = { | ||
| id: 'demo', | ||
| intent: 'documentation', | ||
| tags: ['documentation'], | ||
| description: 'test persona', | ||
| skills: [], | ||
| harness: 'claude', | ||
| model: 'anthropic/claude-3-5-sonnet', | ||
| systemPrompt: 'be helpful', | ||
| harnessSettings: { reasoning: 'medium', timeoutSeconds: 300 }, | ||
| cloud: true | ||
| }; | ||
|
|
||
| const deployment = { | ||
| id: 'deployment_123', | ||
| triggerKind: 'inbox' as const, | ||
| parentDeploymentId: null | ||
| }; | ||
|
|
||
| function defaultsFor(overrides: { | ||
| workspaceId?: string; | ||
| agentId?: string; | ||
| env?: NodeJS.ProcessEnv; | ||
| } = {}) { | ||
| return createCloudRuntimeDefaults({ | ||
| persona, | ||
| agent: { | ||
| id: overrides.agentId ?? 'agent_parent', | ||
| deployedName: 'demo', | ||
| spawnedByAgentId: null | ||
| }, | ||
| deployment, | ||
| workspaceId: overrides.workspaceId ?? 'ws_test', | ||
| log: () => { | ||
| /* keep test output quiet */ | ||
| }, | ||
| env: { | ||
| WORKFORCE_SANDBOX_ROOT: '/tmp', | ||
| ...overrides.env | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| test('createCloudRuntimeDefaults omits team without cloud env or path ids', () => { | ||
| assert.equal(defaultsFor().team, undefined); | ||
| assert.equal(defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| }, | ||
| workspaceId: '' | ||
| }).team, undefined); | ||
| assert.equal(defaultsFor({ | ||
| agentId: '', | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }).team, undefined); | ||
| }); | ||
|
|
||
| test('createCloudRuntimeDefaults attaches team with cloud env and path ids', () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
|
|
||
| assert.equal(typeof defaults.team?.spawn, 'function'); | ||
| assert.equal(typeof defaults.team?.attach, 'function'); | ||
| }); | ||
|
|
||
| test('ctx.team.spawn posts documented body and returns seeded handle', async () => { | ||
| const calls: Array<{ url: string; init?: RequestInit }> = []; | ||
| await withFetch(async (input, init) => { | ||
| calls.push({ url: String(input), init }); | ||
| return jsonResponse({ | ||
| teamId: 'team_123', | ||
| channel: 'team-team_123', | ||
| sharedMountRoot: '/teams/team_123', | ||
| status: 'starting', | ||
| members: [] | ||
| }, { status: 201 }); | ||
| }, async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test/' | ||
| } | ||
| }); | ||
| const handle = await defaults.team!.spawn({ | ||
| task: 'Refactor auth', | ||
| teamPrompt: 'Coordinate through the team board', | ||
| members: [{ name: 'lead', persona: 'relay-orchestrator', role: 'orchestrator' }], | ||
| sharedMount: 'issue-421', | ||
| ttlSeconds: 3600, | ||
| maxMembers: 8 | ||
| }); | ||
|
|
||
| assert.equal(handle.teamId, 'team_123'); | ||
| assert.equal(handle.channel, 'team-team_123'); | ||
| assert.equal(handle.sharedMountRoot, '/teams/team_123'); | ||
| }); | ||
|
|
||
| assert.equal(calls.length, 1); | ||
| assert.equal(calls[0].url, 'https://cloud.example.test/api/v1/workspaces/ws_test/agents/agent_parent/team'); | ||
| assert.equal(calls[0].init?.method, 'POST'); | ||
| assert.deepEqual(calls[0].init?.headers, { | ||
| accept: 'application/json', | ||
| 'content-type': 'application/json', | ||
| authorization: 'Bearer workspace-token' | ||
| }); | ||
| assert.deepEqual(JSON.parse(String(calls[0].init?.body)), { | ||
| task: 'Refactor auth', | ||
| teamPrompt: 'Coordinate through the team board', | ||
| members: [{ name: 'lead', persona: 'relay-orchestrator', role: 'orchestrator' }], | ||
| sharedMount: 'issue-421', | ||
| ttlSeconds: 3600, | ||
| maxMembers: 8 | ||
| }); | ||
| }); | ||
|
|
||
| test('ctx.team.spawn throws when cloud response omits teamId', async () => { | ||
| await withFetch(async () => jsonResponse({}, { status: 201 }), async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
|
|
||
| await assert.rejects( | ||
| () => defaults.team!.spawn({ task: 'Refactor auth', members: [] }), | ||
| /ctx\.team\.spawn\(\): cloud response missing teamId/ | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| test('ctx.team.completion polls until succeeded and returns team result', async () => { | ||
| const statuses = [ | ||
| { teamId: 'team_123', status: 'running', members: [], results: {}, summary: '' }, | ||
| { | ||
| teamId: 'team_123', | ||
| status: 'succeeded', | ||
| members: [{ name: 'impl', status: 'succeeded' }], | ||
| results: { impl: { status: 'succeeded', output: 'done', resultId: 'result_1' } }, | ||
| summary: 'all done' | ||
| } | ||
| ]; | ||
| await withFetch(async (input, init) => { | ||
| if (String(input).endsWith('/team')) { | ||
| return jsonResponse({ teamId: 'team_123', channel: 'team-team_123', sharedMountRoot: '/teams/team_123' }, { status: 201 }); | ||
| } | ||
| assert.equal(init?.method, 'GET'); | ||
| return jsonResponse(statuses.shift() ?? statuses[0]); | ||
| }, async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
| const handle = await defaults.team!.spawn({ task: 'Refactor auth', members: [] }); | ||
|
|
||
| assert.deepEqual(await handle.completion(), { | ||
| status: 'succeeded', | ||
| members: { impl: { status: 'succeeded', output: 'done', resultId: 'result_1' } }, | ||
| summary: 'all done' | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| test('ctx.team.completion maps terminal failure statuses', async () => { | ||
| for (const terminal of ['failed', 'timed_out', 'cancelled'] as const) { | ||
| await withFetch(async (input) => { | ||
| if (String(input).endsWith('/team')) { | ||
| return jsonResponse({ teamId: `team_${terminal}` }, { status: 201 }); | ||
| } | ||
| return jsonResponse({ | ||
| teamId: `team_${terminal}`, | ||
| status: terminal, | ||
| results: { impl: { status: terminal, output: terminal } }, | ||
| summary: terminal | ||
| }); | ||
| }, async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
| const handle = await defaults.team!.spawn({ task: terminal, members: [] }); | ||
|
|
||
| assert.deepEqual(await handle.completion(), { | ||
| status: terminal, | ||
| members: { impl: { status: terminal, output: terminal } }, | ||
| summary: terminal | ||
| }); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| test('ctx.team.attach derives handle fields and confirms status endpoint', async () => { | ||
| const calls: Array<{ url: string; init?: RequestInit }> = []; | ||
| await withFetch(async (input, init) => { | ||
| calls.push({ url: String(input), init }); | ||
| return jsonResponse({ teamId: 'team_abc', status: 'running', members: [], results: {}, summary: '' }); | ||
| }, async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
| const handle = await defaults.team!.attach(' team_abc '); | ||
|
|
||
| assert.equal(handle.teamId, 'team_abc'); | ||
| assert.equal(handle.channel, 'team-team_abc'); | ||
| assert.equal(handle.sharedMountRoot, '/teams/team_abc'); | ||
| assert.deepEqual(await handle.status(), { | ||
| teamId: 'team_abc', | ||
| status: 'running', | ||
| members: [], | ||
| results: {}, | ||
| summary: '' | ||
| }); | ||
| }); | ||
|
|
||
| assert.deepEqual(calls.map((call) => [call.init?.method, call.url]), [ | ||
| ['GET', 'https://cloud.example.test/api/v1/workspaces/ws_test/teams/team_abc'], | ||
| ['GET', 'https://cloud.example.test/api/v1/workspaces/ws_test/teams/team_abc'] | ||
| ]); | ||
| }); | ||
|
|
||
| test('ctx.team.cancel posts to the cancel endpoint and throws on non-2xx', async () => { | ||
| const calls: Array<{ url: string; init?: RequestInit }> = []; | ||
| await withFetch(async (input, init) => { | ||
| calls.push({ url: String(input), init }); | ||
| if (String(input).endsWith('/cancel')) return new Response(null, { status: 204 }); | ||
| return jsonResponse({ teamId: 'team_cancel', status: 'running' }); | ||
| }, async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
| const handle = await defaults.team!.attach('team_cancel'); | ||
|
|
||
| await handle.cancel(); | ||
| }); | ||
|
|
||
| assert.deepEqual(calls.map((call) => [call.init?.method, call.url]), [ | ||
| ['GET', 'https://cloud.example.test/api/v1/workspaces/ws_test/teams/team_cancel'], | ||
| ['POST', 'https://cloud.example.test/api/v1/workspaces/ws_test/teams/team_cancel/cancel'] | ||
| ]); | ||
|
|
||
| await withFetch(async (input) => { | ||
| if (String(input).endsWith('/cancel')) return new Response('nope', { status: 500, statusText: 'Server Error' }); | ||
| return jsonResponse({ teamId: 'team_cancel', status: 'running' }); | ||
| }, async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
| const handle = await defaults.team!.attach('team_cancel'); | ||
|
|
||
| await assert.rejects(() => handle.cancel(), /ctx\.team\.attach\("team_cancel"\)\.cancel\(\): 500 Server Error - nope/); | ||
| }); | ||
| }); | ||
|
|
||
| test('ctx.team.completion retries transient status errors within the budget', async () => { | ||
| let statusCalls = 0; | ||
| await withFetch(async (input) => { | ||
| if (String(input).endsWith('/team')) { | ||
| return jsonResponse({ teamId: 'team_retry' }, { status: 201 }); | ||
| } | ||
| statusCalls += 1; | ||
| if (statusCalls === 1) return new Response('try again', { status: 503, statusText: 'Unavailable' }); | ||
| return jsonResponse({ teamId: 'team_retry', status: 'succeeded', results: {}, summary: 'done' }); | ||
| }, async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
| const handle = await defaults.team!.spawn({ task: 'retry', members: [] }); | ||
|
|
||
| assert.deepEqual(await handle.completion(), { | ||
| status: 'succeeded', | ||
| members: {}, | ||
| summary: 'done' | ||
| }); | ||
| assert.equal(statusCalls, 2); | ||
| }); | ||
| }); | ||
|
|
||
| test('ctx.team.completion throws once the transient retry budget is exhausted', async () => { | ||
| let statusCalls = 0; | ||
| await withFetch(async (input) => { | ||
| if (String(input).endsWith('/team')) { | ||
| return jsonResponse({ teamId: 'team_exhaust' }, { status: 201 }); | ||
| } | ||
| statusCalls += 1; | ||
| return new Response('try again', { status: 503, statusText: 'Unavailable' }); | ||
| }, async () => { | ||
| const defaults = defaultsFor({ | ||
| env: { | ||
| WORKFORCE_WORKSPACE_TOKEN: 'workspace-token', | ||
| WORKFORCE_CLOUD_BASE_URL: 'https://cloud.example.test' | ||
| } | ||
| }); | ||
| const handle = await defaults.team!.spawn({ task: 'exhaust', members: [] }); | ||
|
|
||
| // Budget is exhausted after MAX_TRANSIENT_ERRORS retries; the next retryable | ||
| // failure is rethrown verbatim (not converted into the deadline-timeout error). | ||
| await assert.rejects( | ||
| () => handle.completion(), | ||
| /ctx\.team\.attach\("team_exhaust"\)\.status\(\): 503 Unavailable - try again/ | ||
| ); | ||
| // MAX_TRANSIENT_ERRORS (3) retries that continue, then one more poll that throws. | ||
| assert.equal(statusCalls, 4); | ||
| }); | ||
| }); | ||
|
|
||
| function jsonResponse(body: unknown, init: ResponseInit = {}): Response { | ||
| return new Response(JSON.stringify(body), { | ||
| ...init, | ||
| headers: { | ||
| 'content-type': 'application/json', | ||
| ...init.headers | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Safe under node:test because top-level tests run sequentially by default | ||
| // (no `{ concurrency: true }` here), so the global swap never overlaps; the | ||
| // `finally` restore guarantees the original fetch is back even on failure. | ||
| async function withFetch( | ||
| fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> | Response, | ||
| run: () => Promise<void> | ||
| ): Promise<void> { | ||
| const originalFetch = globalThis.fetch; | ||
| globalThis.fetch = fetchImpl as typeof fetch; | ||
| try { | ||
| await run(); | ||
| } finally { | ||
| globalThis.fetch = originalFetch; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In Node.jsnode:test, what are the default concurrency semantics for top-level tests in a single file, and what is the recommended way to safely mock globalThis.fetch per test?💡 Result:
Default concurrency semantics for top-level tests in a single file (node:test) 1) Top-level tests (in a file) are not run in parallel by default. When process-level test isolation is disabled (the default), Node loads each test file into the same runner process, and “top level tests are executed with a concurrency of one.”[1] 2) Subtests are sequential unless you opt into parallelism. The node:test test option “concurrency” defaults to false, meaning “only one test runs at a time.” If concurrency is unspecified, subtests inherit this value from their parent; the default therefore keeps them sequential within the suite unless you set concurrency explicitly.[2][1] (How this changes with process-level isolation) If process-level test isolation is enabled, each test file runs in a separate child process; the number of child processes in parallel is controlled by --test-concurrency.[2] Recommended way to safely mock globalThis.fetch per test Use node:test’s built-in per-test mocking utilities (module mocks), rather than overwriting globalThis.fetch in shared global state. Specifically, register the mock before importing the module that uses fetch, and then dynamically import the module under test. The node:test docs/example show using mock.module(...) and then doing await import(...) after the mock is registered, ensuring each test gets its own mocked implementation in the right order.[3] Why: because when files share a context (no process isolation), tests can modify global state and affect other tests.[1] Mocking fetch via node:test’s module-mock mechanism avoids relying on a shared globalThis.fetch across concurrent tests. Practical pattern (sketch) - Inside your test callback: - call mock.module(...) to replace the module (or export) that provides fetch usage - then dynamically import the module under test If you truly must set globalThis.fetch, ensure you restore it in the same test (e.g., within afterEach / finally), because global mutation is inherently shared when isolation is off.[1]
Citations:
Avoid shared global
fetchswapping—use per-test mocking instead.In
packages/runtime/src/cloud-defaults.test.ts(withFetch, lines 345-355),globalThis.fetchis replaced for the duration ofrun.node:testexecutes top-level tests in a single file with concurrency=1 by default, so same-file overlap is unlikely—but this is still brittle if test/process concurrency settings change or if other tests in the same process also touchglobalThis.fetch. Prefernode:test’s per-test module mocking (e.g.,mock.module(...)+ dynamicimport) or dependency injection so tests don’t rely on process-global mutation.🤖 Prompt for AI Agents