agent-relay 3.0.1 → 3.1.0
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.
- package/README.md +37 -244
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +342 -60
- package/dist/src/cli/commands/core.d.ts +2 -0
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +9 -2
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +87 -28
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/package.json +9 -8
- package/packages/acp-bridge/README.md +50 -67
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +169 -64
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js +76 -9
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +1 -1
- package/packages/sdk/dist/__tests__/facade.test.js +48 -0
- package/packages/sdk/dist/__tests__/facade.test.js.map +1 -1
- package/packages/sdk/dist/__tests__/integration.test.js +11 -5
- package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
- package/packages/sdk/dist/__tests__/unit.test.js +36 -0
- package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +36 -3
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +142 -9
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/protocol.d.ts +7 -1
- package/packages/sdk/dist/protocol.d.ts.map +1 -1
- package/packages/sdk/dist/relay.d.ts +74 -11
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +175 -27
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +71 -36
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +1 -1
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/contract-fixtures.test.ts +88 -9
- package/packages/sdk/src/__tests__/error-scenarios.test.ts +1 -1
- package/packages/sdk/src/__tests__/facade.test.ts +68 -0
- package/packages/sdk/src/__tests__/idle-nudge.test.ts +205 -257
- package/packages/sdk/src/__tests__/integration.test.ts +11 -5
- package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +277 -13
- package/packages/sdk/src/__tests__/swarm-coordinator.test.ts +1 -0
- package/packages/sdk/src/__tests__/unit.test.ts +44 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +67 -7
- package/packages/sdk/src/__tests__/workflow-trajectory.test.ts +4 -5
- package/packages/sdk/src/client.ts +195 -14
- package/packages/sdk/src/examples/workflows/runner-idle-refactor.yaml +306 -0
- package/packages/sdk/src/protocol.ts +7 -2
- package/packages/sdk/src/relay.ts +271 -38
- package/packages/sdk/src/workflows/runner.ts +73 -42
- package/packages/sdk/src/workflows/schema.json +1 -1
- package/packages/sdk/src/workflows/types.ts +1 -1
- package/packages/sdk/vitest.config.ts +1 -0
- package/packages/sdk-py/README.md +89 -102
- package/packages/sdk-py/agent_relay/__init__.py +16 -19
- package/packages/sdk-py/pyproject.toml +6 -2
- package/packages/sdk-py/src/agent_relay/__init__.py +35 -1
- package/packages/sdk-py/src/agent_relay/client.py +776 -0
- package/packages/sdk-py/src/agent_relay/models.py +27 -0
- package/packages/sdk-py/src/agent_relay/protocol.py +114 -0
- package/packages/sdk-py/src/agent_relay/relay.py +860 -0
- package/packages/sdk-py/tests/test_relay_lifecycle_hooks.py +250 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/scripts/postinstall.js +35 -162
- package/packages/sdk/.trajectories/active/traj_1771875803391_84ca57b2.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891934534_06504121.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891957929_211afc4e.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891982509_38c84638.json +0 -50
- package/packages/sdk/.trajectories/completed/traj_1771875803188_cd6d181c.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803204_f2aeb8c8.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803210_d65f3f1a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803218_e454a25d.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803223_d7a64815.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803227_7e56da5b.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803235_4fbf93b4.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803243_47931c71.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803258_3816f3fe.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803268_8061140e.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803326_ae6f9c78.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875808396_cbde0a6c.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875812026_aa2442bb.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875815431_c2c656c5.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875818645_3a4dbf02.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891934403_24923c03.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934421_dca16e24.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934430_057706f7.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934442_faf97382.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934454_5542ecd5.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934464_12202a08.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934487_94378275.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934503_ca728c13.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934519_100af69a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934536_62ad39d9.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934553_d6798a52.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891939537_541c8096.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891942985_36ab9a4d.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891946453_e8a6e05f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891949838_5de0de84.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891957807_0ecfb4f4.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957827_c4539239.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957836_91168b48.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957848_8c5cad0b.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957857_0986b293.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957872_8a3113af.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957884_0bb85208.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957892_86c75e2e.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957907_98ca0e6f.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957918_d9091231.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957931_dcaf77ed.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891962931_eb1fdee2.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891966262_9061a93f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891969915_1adaba19.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891973588_f08b79e9.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891982421_f1985bce.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982432_e7a84163.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982447_369b842a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982469_5fc45199.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982495_454c7cb3.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982514_08098e03.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982526_b351d778.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982533_fa542d83.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982540_18ab24dc.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982544_5b4fa163.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982548_c13f089a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891987510_23f6da1f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891991466_912c2e04.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891994891_60604be2.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891998370_cfaf9b8b.json +0 -91
- package/packages/sdk/bin/agent-relay-broker +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Idle nudge detection and escalation tests.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Covers both modes:
|
|
5
|
+
* - No idleNudge config: idle is treated as completion.
|
|
6
|
+
* - idleNudge config enabled: waitForExit timeout drives nudges/escalation.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
@@ -18,7 +19,7 @@ const mockFetch = vi.fn().mockResolvedValue({
|
|
|
18
19
|
});
|
|
19
20
|
vi.stubGlobal('fetch', mockFetch);
|
|
20
21
|
|
|
21
|
-
// ── Mock RelayCast SDK
|
|
22
|
+
// ── Mock RelayCast SDK ────────────────────────────────────────────────────────
|
|
22
23
|
|
|
23
24
|
const mockRelaycastAgent = {
|
|
24
25
|
send: vi.fn().mockResolvedValue(undefined),
|
|
@@ -52,9 +53,8 @@ vi.mock('@relaycast/sdk', () => ({
|
|
|
52
53
|
RelayError: MockRelayError,
|
|
53
54
|
}));
|
|
54
55
|
|
|
55
|
-
// ── Mock AgentRelay
|
|
56
|
+
// ── Mock AgentRelay ───────────────────────────────────────────────────────────
|
|
56
57
|
|
|
57
|
-
/** Control how waitForExit / waitForIdle resolve in each test. */
|
|
58
58
|
let waitForExitFn: (ms?: number) => Promise<'exited' | 'timeout' | 'released'>;
|
|
59
59
|
let waitForIdleFn: (ms?: number) => Promise<'idle' | 'timeout' | 'exited'>;
|
|
60
60
|
|
|
@@ -84,15 +84,15 @@ vi.mock('../relay.js', () => ({
|
|
|
84
84
|
spawnPty: vi.fn().mockResolvedValue(mockAgent),
|
|
85
85
|
human: vi.fn().mockReturnValue(mockHuman),
|
|
86
86
|
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
87
|
+
onBrokerStderr: vi.fn().mockReturnValue(() => {}),
|
|
87
88
|
onWorkerOutput: null,
|
|
88
89
|
listAgentsRaw: vi.fn().mockResolvedValue([]),
|
|
89
90
|
})),
|
|
90
91
|
}));
|
|
91
92
|
|
|
92
|
-
// Import after mocking
|
|
93
93
|
const { WorkflowRunner } = await import('../workflows/runner.js');
|
|
94
94
|
|
|
95
|
-
// ── Test fixtures
|
|
95
|
+
// ── Test fixtures ─────────────────────────────────────────────────────────────
|
|
96
96
|
|
|
97
97
|
function makeDb(): WorkflowDb {
|
|
98
98
|
const runs = new Map<string, WorkflowRunRow>();
|
|
@@ -135,11 +135,14 @@ function makeConfig(overrides: Partial<RelayYamlConfig> = {}): RelayYamlConfig {
|
|
|
135
135
|
steps: [{ name: 'step-1', agent: 'agent-a', task: 'Do step 1' }],
|
|
136
136
|
},
|
|
137
137
|
],
|
|
138
|
+
trajectories: false,
|
|
138
139
|
...overrides,
|
|
139
140
|
};
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
function never<T>(): Promise<T> {
|
|
144
|
+
return new Promise(() => {});
|
|
145
|
+
}
|
|
143
146
|
|
|
144
147
|
describe('Idle Nudge Detection', () => {
|
|
145
148
|
let db: WorkflowDb;
|
|
@@ -150,289 +153,234 @@ describe('Idle Nudge Detection', () => {
|
|
|
150
153
|
db = makeDb();
|
|
151
154
|
runner = new WorkflowRunner({ db, workspaceId: 'ws-test' });
|
|
152
155
|
|
|
153
|
-
// Default: agent exits immediately (no idle)
|
|
154
156
|
waitForExitFn = vi.fn().mockResolvedValue('exited');
|
|
155
157
|
waitForIdleFn = vi.fn().mockResolvedValue('timeout');
|
|
156
158
|
});
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
160
|
+
describe('idleNudge enabled', () => {
|
|
161
|
+
it('sends direct nudge then completes when exit follows', async () => {
|
|
162
|
+
let exitCallCount = 0;
|
|
163
|
+
waitForExitFn = vi.fn().mockImplementation(() => {
|
|
164
|
+
exitCallCount++;
|
|
165
|
+
return Promise.resolve(exitCallCount === 1 ? 'timeout' : 'exited');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const run = await runner.execute(
|
|
169
|
+
makeConfig({
|
|
170
|
+
swarm: {
|
|
171
|
+
pattern: 'mesh',
|
|
172
|
+
idleNudge: { nudgeAfterMs: 100, escalateAfterMs: 100, maxNudges: 1 },
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
'default'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(run.status).toBe('completed');
|
|
179
|
+
expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
|
|
180
|
+
expect(mockHumanSendMessage).toHaveBeenCalledWith(
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
to: 'test-agent-abc',
|
|
183
|
+
text: expect.stringContaining('/exit'),
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
expect(mockRelease).not.toHaveBeenCalled();
|
|
187
|
+
expect(waitForIdleFn).not.toHaveBeenCalled();
|
|
185
188
|
});
|
|
186
189
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
it('uses hub fallback behavior without failing when hub is not active', async () => {
|
|
191
|
+
let exitCallCount = 0;
|
|
192
|
+
waitForExitFn = vi.fn().mockImplementation(() => {
|
|
193
|
+
exitCallCount++;
|
|
194
|
+
return Promise.resolve(exitCallCount === 1 ? 'timeout' : 'exited');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const run = await runner.execute(
|
|
198
|
+
makeConfig({
|
|
199
|
+
swarm: {
|
|
200
|
+
pattern: 'hub-spoke',
|
|
201
|
+
idleNudge: { nudgeAfterMs: 100, escalateAfterMs: 100, maxNudges: 1 },
|
|
202
|
+
},
|
|
203
|
+
agents: [
|
|
204
|
+
{ name: 'lead', cli: 'claude', role: 'Lead coordinator' },
|
|
205
|
+
{ name: 'worker', cli: 'claude' },
|
|
206
|
+
],
|
|
207
|
+
workflows: [
|
|
208
|
+
{
|
|
209
|
+
name: 'default',
|
|
210
|
+
steps: [{ name: 'step-1', agent: 'worker', task: 'Do work' }],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
}),
|
|
214
|
+
'default'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(run.status).toBe('completed');
|
|
218
|
+
expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
|
|
192
219
|
});
|
|
193
220
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (idleCallCount === 1) return Promise.resolve('idle');
|
|
212
|
-
return Promise.resolve('exited');
|
|
213
|
-
});
|
|
214
|
-
let exitCallCount = 0;
|
|
215
|
-
waitForExitFn = vi.fn().mockImplementation(() => {
|
|
216
|
-
exitCallCount++;
|
|
217
|
-
if (exitCallCount === 1) return new Promise(() => {});
|
|
218
|
-
return Promise.resolve('exited');
|
|
221
|
+
it('force-releases after maxNudges is exceeded', async () => {
|
|
222
|
+
waitForExitFn = vi.fn().mockResolvedValue('timeout');
|
|
223
|
+
|
|
224
|
+
const run = await runner.execute(
|
|
225
|
+
makeConfig({
|
|
226
|
+
swarm: {
|
|
227
|
+
pattern: 'dag',
|
|
228
|
+
idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
'default'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(run.status).toBe('completed');
|
|
235
|
+
expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
|
|
236
|
+
expect(mockRelease).toHaveBeenCalledTimes(1);
|
|
237
|
+
expect(waitForIdleFn).not.toHaveBeenCalled();
|
|
219
238
|
});
|
|
220
239
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
240
|
+
it('force-releases after multiple nudges', async () => {
|
|
241
|
+
waitForExitFn = vi.fn().mockResolvedValue('timeout');
|
|
242
|
+
|
|
243
|
+
const run = await runner.execute(
|
|
244
|
+
makeConfig({
|
|
245
|
+
swarm: {
|
|
246
|
+
pattern: 'dag',
|
|
247
|
+
idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 3 },
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
'default'
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(run.status).toBe('completed');
|
|
254
|
+
expect(mockHumanSendMessage).toHaveBeenCalledTimes(3);
|
|
255
|
+
expect(mockRelease).toHaveBeenCalledTimes(1);
|
|
236
256
|
});
|
|
237
257
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const config = makeConfig({
|
|
260
|
-
swarm: {
|
|
261
|
-
pattern: 'hub-spoke',
|
|
262
|
-
idleNudge: { nudgeAfterMs: 100, escalateAfterMs: 100, maxNudges: 1 },
|
|
263
|
-
},
|
|
264
|
-
agents: [{ name: 'lead', cli: 'claude', role: 'Lead coordinator' }],
|
|
265
|
-
workflows: [
|
|
266
|
-
{
|
|
267
|
-
name: 'default',
|
|
268
|
-
steps: [{ name: 'step-1', agent: 'lead', task: 'Coordinate work' }],
|
|
269
|
-
},
|
|
270
|
-
],
|
|
258
|
+
it('emits step:nudged event', async () => {
|
|
259
|
+
let exitCallCount = 0;
|
|
260
|
+
waitForExitFn = vi.fn().mockImplementation(() => {
|
|
261
|
+
exitCallCount++;
|
|
262
|
+
return Promise.resolve(exitCallCount === 1 ? 'timeout' : 'exited');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const events: Array<{ type: string }> = [];
|
|
266
|
+
runner.on((event) => events.push(event));
|
|
267
|
+
|
|
268
|
+
await runner.execute(
|
|
269
|
+
makeConfig({
|
|
270
|
+
swarm: {
|
|
271
|
+
pattern: 'dag',
|
|
272
|
+
idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
'default'
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(events.filter((e) => e.type === 'step:nudged')).toHaveLength(1);
|
|
271
279
|
});
|
|
272
280
|
|
|
273
|
-
|
|
281
|
+
it('emits step:force-released event on escalation', async () => {
|
|
282
|
+
waitForExitFn = vi.fn().mockResolvedValue('timeout');
|
|
274
283
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
|
|
278
|
-
});
|
|
284
|
+
const events: Array<{ type: string }> = [];
|
|
285
|
+
runner.on((event) => events.push(event));
|
|
279
286
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
287
|
+
await runner.execute(
|
|
288
|
+
makeConfig({
|
|
289
|
+
swarm: {
|
|
290
|
+
pattern: 'dag',
|
|
291
|
+
idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
'default'
|
|
295
|
+
);
|
|
284
296
|
|
|
285
|
-
|
|
286
|
-
swarm: {
|
|
287
|
-
pattern: 'dag',
|
|
288
|
-
idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
|
|
289
|
-
},
|
|
297
|
+
expect(events.filter((e) => e.type === 'step:force-released')).toHaveLength(1);
|
|
290
298
|
});
|
|
291
299
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
300
|
+
it('uses defaults when idleNudge is empty object', async () => {
|
|
301
|
+
waitForExitFn = vi.fn().mockResolvedValue('timeout');
|
|
302
|
+
|
|
303
|
+
const run = await runner.execute(
|
|
304
|
+
makeConfig({
|
|
305
|
+
swarm: {
|
|
306
|
+
pattern: 'dag',
|
|
307
|
+
idleNudge: {},
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
'default'
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
expect(run.status).toBe('completed');
|
|
314
|
+
// default maxNudges is 1
|
|
315
|
+
expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
|
|
316
|
+
expect(mockRelease).toHaveBeenCalledTimes(1);
|
|
309
317
|
});
|
|
310
318
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
319
|
+
it('respects overall timeout during nudge loop', async () => {
|
|
320
|
+
// Each waitForExit call takes 100ms (real timer), but the overall timeout
|
|
321
|
+
// is only 80ms. After the first call (~100ms elapsed), the loop detects
|
|
322
|
+
// that remaining time is exhausted and returns 'timeout'.
|
|
323
|
+
waitForExitFn = vi
|
|
324
|
+
.fn()
|
|
325
|
+
.mockImplementation(
|
|
326
|
+
() => new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 100))
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const run = await runner.execute(
|
|
330
|
+
makeConfig({
|
|
331
|
+
swarm: {
|
|
332
|
+
pattern: 'dag',
|
|
333
|
+
idleNudge: { nudgeAfterMs: 10, escalateAfterMs: 10, maxNudges: 10 },
|
|
334
|
+
},
|
|
335
|
+
agents: [{ name: 'agent-a', cli: 'claude', constraints: { timeoutMs: 80 } }],
|
|
336
|
+
}),
|
|
337
|
+
'default'
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(run.status).toBe('failed');
|
|
341
|
+
expect(run.error).toContain('timed out');
|
|
331
342
|
});
|
|
332
|
-
|
|
333
|
-
// The step has a short timeout — should not loop forever
|
|
334
|
-
const run = await runner.execute(config, 'default');
|
|
335
|
-
// Either completed (force-released) or failed (timeout) — either is acceptable
|
|
336
|
-
expect(['completed', 'failed']).toContain(run.status);
|
|
337
343
|
});
|
|
338
344
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (idleCallCount === 1) return Promise.resolve('idle');
|
|
344
|
-
return Promise.resolve('exited');
|
|
345
|
-
});
|
|
346
|
-
let exitCallCount = 0;
|
|
347
|
-
waitForExitFn = vi.fn().mockImplementation(() => {
|
|
348
|
-
exitCallCount++;
|
|
349
|
-
if (exitCallCount === 1) return new Promise(() => {});
|
|
350
|
-
return Promise.resolve('exited');
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const events: Array<{ type: string }> = [];
|
|
354
|
-
const config = makeConfig({
|
|
355
|
-
swarm: {
|
|
356
|
-
pattern: 'dag',
|
|
357
|
-
idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
runner.on((event) => events.push(event));
|
|
362
|
-
await runner.execute(config, 'default');
|
|
363
|
-
|
|
364
|
-
const nudgeEvents = events.filter((e) => e.type === 'step:nudged');
|
|
365
|
-
expect(nudgeEvents).toHaveLength(1);
|
|
366
|
-
});
|
|
345
|
+
describe('Idle = done (no idleNudge config)', () => {
|
|
346
|
+
it('idle fires first: releases agent and completes step', async () => {
|
|
347
|
+
waitForIdleFn = vi.fn().mockResolvedValue('idle');
|
|
348
|
+
waitForExitFn = vi.fn().mockImplementation(() => never());
|
|
367
349
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
waitForExitFn = vi.fn().mockImplementation(() => new Promise(() => {}));
|
|
350
|
+
const run = await runner.execute(makeConfig(), 'default');
|
|
351
|
+
const steps = await db.getStepsByRunId(run.id);
|
|
371
352
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
idleNudge: { nudgeAfterMs: 50, escalateAfterMs: 50, maxNudges: 1 },
|
|
377
|
-
},
|
|
353
|
+
expect(run.status).toBe('completed');
|
|
354
|
+
expect(steps).toHaveLength(1);
|
|
355
|
+
expect(steps[0]?.status).toBe('completed');
|
|
356
|
+
expect(mockRelease).toHaveBeenCalledTimes(1);
|
|
378
357
|
});
|
|
379
358
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const forceReleasedEvents = events.filter((e) => e.type === 'step:force-released');
|
|
384
|
-
expect(forceReleasedEvents).toHaveLength(1);
|
|
385
|
-
});
|
|
359
|
+
it('exit fires first: completes without idle-based release', async () => {
|
|
360
|
+
waitForExitFn = vi.fn().mockResolvedValue('exited');
|
|
361
|
+
waitForIdleFn = vi.fn().mockResolvedValue('timeout');
|
|
386
362
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// First idle → nudge. Second idle → escalate (maxNudges: 1)
|
|
390
|
-
waitForIdleFn = vi.fn().mockImplementation(() => {
|
|
391
|
-
idleCallCount++;
|
|
392
|
-
return Promise.resolve('idle');
|
|
393
|
-
});
|
|
394
|
-
waitForExitFn = vi.fn().mockImplementation(() => new Promise(() => {}));
|
|
363
|
+
const run = await runner.execute(makeConfig(), 'default');
|
|
364
|
+
const steps = await db.getStepsByRunId(run.id);
|
|
395
365
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
},
|
|
366
|
+
expect(run.status).toBe('completed');
|
|
367
|
+
expect(steps).toHaveLength(1);
|
|
368
|
+
expect(steps[0]?.status).toBe('completed');
|
|
369
|
+
expect(mockRelease).not.toHaveBeenCalled();
|
|
401
370
|
});
|
|
402
371
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// 1 nudge sent, then force-released
|
|
407
|
-
expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
|
|
408
|
-
expect(mockRelease).toHaveBeenCalledTimes(1);
|
|
409
|
-
});
|
|
372
|
+
it('both timeout: fails step with timeout error', async () => {
|
|
373
|
+
waitForExitFn = vi.fn().mockResolvedValue('timeout');
|
|
374
|
+
waitForIdleFn = vi.fn().mockResolvedValue('timeout');
|
|
410
375
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
waitForIdleFn = vi.fn().mockImplementation(() => {
|
|
414
|
-
idleCallCount++;
|
|
415
|
-
if (idleCallCount === 1) return Promise.resolve('idle');
|
|
416
|
-
return Promise.resolve('exited');
|
|
417
|
-
});
|
|
418
|
-
let exitCallCount = 0;
|
|
419
|
-
waitForExitFn = vi.fn().mockImplementation(() => {
|
|
420
|
-
exitCallCount++;
|
|
421
|
-
if (exitCallCount === 1) return new Promise(() => {});
|
|
422
|
-
return Promise.resolve('exited');
|
|
423
|
-
});
|
|
376
|
+
const run = await runner.execute(makeConfig(), 'default');
|
|
377
|
+
const steps = await db.getStepsByRunId(run.id);
|
|
424
378
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
379
|
+
expect(run.status).toBe('failed');
|
|
380
|
+
expect(run.error).toContain('timed out');
|
|
381
|
+
expect(steps).toHaveLength(1);
|
|
382
|
+
expect(steps[0]?.status).toBe('failed');
|
|
383
|
+
expect(steps[0]?.error).toContain('timed out');
|
|
430
384
|
});
|
|
431
|
-
|
|
432
|
-
const run = await runner.execute(config, 'default');
|
|
433
|
-
|
|
434
|
-
expect(run.status).toBe('completed');
|
|
435
|
-
// Default maxNudges: 1, so one nudge should have been sent
|
|
436
|
-
expect(mockHumanSendMessage).toHaveBeenCalledTimes(1);
|
|
437
385
|
});
|
|
438
386
|
});
|
|
@@ -24,7 +24,12 @@ function resolveBundledBinaryPath(): string {
|
|
|
24
24
|
before(async () => {
|
|
25
25
|
if (process.env.RELAY_API_KEY?.trim()) return;
|
|
26
26
|
const ws = await RelayCast.createWorkspace(`sdk-test-${Date.now().toString(36)}`);
|
|
27
|
-
|
|
27
|
+
const workspace = ws as { apiKey?: string; api_key?: string };
|
|
28
|
+
const apiKey = workspace.apiKey ?? workspace.api_key;
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
throw new Error('RelayCast.createWorkspace() did not return an API key');
|
|
31
|
+
}
|
|
32
|
+
process.env.RELAY_API_KEY = apiKey;
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
test('sdk can use bundled binary by default', async (t) => {
|
|
@@ -98,7 +103,7 @@ test('sdk can start broker and manage agent lifecycle', async (t) => {
|
|
|
98
103
|
}
|
|
99
104
|
});
|
|
100
105
|
|
|
101
|
-
test('sdk can spawn and release
|
|
106
|
+
test('sdk can spawn and release provider worker with transport override', async (t) => {
|
|
102
107
|
const binaryPath = resolveBinaryPath();
|
|
103
108
|
if (!fs.existsSync(binaryPath)) {
|
|
104
109
|
t.skip(`agent-relay-broker binary not found at ${binaryPath}`);
|
|
@@ -119,17 +124,18 @@ test('sdk can spawn and release headless claude worker', async (t) => {
|
|
|
119
124
|
});
|
|
120
125
|
|
|
121
126
|
try {
|
|
122
|
-
const spawned = await client.
|
|
127
|
+
const spawned = await client.spawnClaude({
|
|
123
128
|
name: spawnedName,
|
|
129
|
+
transport: 'headless',
|
|
124
130
|
channels: ['general'],
|
|
125
131
|
});
|
|
126
132
|
assert.equal(spawned.name, spawnedName);
|
|
127
|
-
assert.equal(spawned.runtime, '
|
|
133
|
+
assert.equal(spawned.runtime, 'headless');
|
|
128
134
|
|
|
129
135
|
const agentsAfterSpawn = await client.listAgents();
|
|
130
136
|
const spawnedAgent = agentsAfterSpawn.find((agent) => agent.name === spawnedName);
|
|
131
137
|
assert.ok(spawnedAgent, 'spawned headless agent should be present in listAgents()');
|
|
132
|
-
assert.equal(spawnedAgent?.runtime, '
|
|
138
|
+
assert.equal(spawnedAgent?.runtime, 'headless');
|
|
133
139
|
|
|
134
140
|
const released = await client.release(spawnedName);
|
|
135
141
|
assert.equal(released.name, spawnedName);
|