agent-relay 3.2.17 → 3.2.21
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/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 +86 -43
- package/dist/src/cli/commands/cloud.d.ts +1 -9
- package/dist/src/cli/commands/cloud.d.ts.map +1 -1
- package/dist/src/cli/commands/cloud.js +326 -323
- package/dist/src/cli/commands/cloud.js.map +1 -1
- package/dist/src/cli/commands/connect.d.ts.map +1 -1
- package/dist/src/cli/commands/connect.js +6 -10
- package/dist/src/cli/commands/connect.js.map +1 -1
- package/package.json +16 -10
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/README.md +36 -0
- package/packages/brand/brand.css +226 -0
- package/packages/brand/package.json +20 -0
- package/packages/cloud/dist/api-client.d.ts +33 -0
- package/packages/cloud/dist/api-client.d.ts.map +1 -0
- package/packages/cloud/dist/api-client.js +123 -0
- package/packages/cloud/dist/api-client.js.map +1 -0
- package/packages/cloud/dist/auth.d.ts +13 -0
- package/packages/cloud/dist/auth.d.ts.map +1 -0
- package/packages/cloud/dist/auth.js +248 -0
- package/packages/cloud/dist/auth.js.map +1 -0
- package/packages/cloud/dist/index.d.ts +5 -0
- package/packages/cloud/dist/index.d.ts.map +1 -0
- package/packages/cloud/dist/index.js +5 -0
- package/packages/cloud/dist/index.js.map +1 -0
- package/packages/cloud/dist/types.d.ts +73 -0
- package/packages/cloud/dist/types.d.ts.map +1 -0
- package/packages/cloud/dist/types.js +19 -0
- package/packages/cloud/dist/types.js.map +1 -0
- package/packages/cloud/dist/workflows.d.ts +34 -0
- package/packages/cloud/dist/workflows.d.ts.map +1 -0
- package/packages/cloud/dist/workflows.js +389 -0
- package/packages/cloud/dist/workflows.js.map +1 -0
- package/packages/cloud/package.json +44 -0
- package/packages/cloud/src/api-client.ts +169 -0
- package/packages/cloud/src/auth.ts +314 -0
- package/packages/cloud/src/index.ts +41 -0
- package/packages/cloud/src/types.ts +97 -0
- package/packages/cloud/src/workflows.ts +539 -0
- package/packages/cloud/tsconfig.json +21 -0
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.js +62 -0
- package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.js.map +1 -0
- package/packages/sdk/dist/workflows/runner.d.ts +4 -0
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +76 -39
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +73 -2
- package/packages/sdk/src/workflows/__tests__/e2big-and-verify.test.ts +117 -0
- package/packages/sdk/src/workflows/runner.ts +105 -38
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserver.swift +2 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/sdk",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"typescript": "^5.7.3"
|
|
113
113
|
},
|
|
114
114
|
"dependencies": {
|
|
115
|
-
"@agent-relay/config": "3.2.
|
|
115
|
+
"@agent-relay/config": "3.2.21",
|
|
116
116
|
"@relaycast/sdk": "^1.1.0",
|
|
117
117
|
"@sinclair/typebox": "^0.34.48",
|
|
118
118
|
"chalk": "^4.1.2",
|
|
@@ -642,8 +642,7 @@ agents:
|
|
|
642
642
|
);
|
|
643
643
|
|
|
644
644
|
expect(ownerAssignments).toContainEqual({ owner: 'relay-worker', specialist: 'relay-worker' });
|
|
645
|
-
expect(run.status).toBe('
|
|
646
|
-
expect(run.error).toContain('verification failed');
|
|
645
|
+
expect(run.status, run.error).toBe('completed');
|
|
647
646
|
|
|
648
647
|
const spawnCalls = (mockRelayInstance.spawnPty as any).mock.calls;
|
|
649
648
|
expect(spawnCalls).toHaveLength(1);
|
|
@@ -652,6 +651,78 @@ agents:
|
|
|
652
651
|
expect(spawnCalls[0][0].name).not.toContain('-review-');
|
|
653
652
|
});
|
|
654
653
|
|
|
654
|
+
it('should spill oversized interactive tasks to a temp file before PTY spawn', async () => {
|
|
655
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'relay-pty-task-'));
|
|
656
|
+
const oversizedBytes = WorkflowRunner.PTY_TASK_ARG_SIZE_LIMIT + 1024;
|
|
657
|
+
let spawnedTask = '';
|
|
658
|
+
let taskFilePath = '';
|
|
659
|
+
let taskFileContents = '';
|
|
660
|
+
runner = new WorkflowRunner({ db, workspaceId: 'ws-test', cwd: tmpDir });
|
|
661
|
+
|
|
662
|
+
mockRelayInstance.spawnPty.mockImplementation(
|
|
663
|
+
async ({ name, task }: { name: string; task?: string }) => {
|
|
664
|
+
spawnedTask = task ?? '';
|
|
665
|
+
const match = spawnedTask.match(/TASK_FILE:(.+)\n/);
|
|
666
|
+
if (match) {
|
|
667
|
+
taskFilePath = match[1].trim();
|
|
668
|
+
taskFileContents = readFileSync(taskFilePath, 'utf-8');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const output = mockSpawnOutputs.shift() ?? 'LEAD_DONE\n';
|
|
672
|
+
queueMicrotask(() => {
|
|
673
|
+
if (typeof mockRelayInstance.onWorkerOutput === 'function') {
|
|
674
|
+
mockRelayInstance.onWorkerOutput({ name, chunk: output });
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
return { ...mockAgent, name };
|
|
679
|
+
}
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
mockSpawnOutputs = ['LEAD_DONE\n'];
|
|
684
|
+
|
|
685
|
+
const run = await runner.execute(
|
|
686
|
+
makeConfig({
|
|
687
|
+
agents: [{ name: 'team-lead', cli: 'claude', role: 'Lead coordinator' }],
|
|
688
|
+
workflows: [
|
|
689
|
+
{
|
|
690
|
+
name: 'default',
|
|
691
|
+
steps: [
|
|
692
|
+
{
|
|
693
|
+
name: 'prepare',
|
|
694
|
+
type: 'deterministic',
|
|
695
|
+
command: `node -e "process.stdout.write('A'.repeat(${oversizedBytes}))"`,
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
name: 'lead-step',
|
|
699
|
+
agent: 'team-lead',
|
|
700
|
+
dependsOn: ['prepare'],
|
|
701
|
+
task: 'Review the injected context below and then print LEAD_DONE:\n{{steps.prepare.output}}\n/exit',
|
|
702
|
+
verification: { type: 'exit_code', value: 0 },
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
}),
|
|
708
|
+
'default'
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
expect(run.status, run.error).toBe('completed');
|
|
712
|
+
expect(spawnedTask).toContain('TASK_FILE:');
|
|
713
|
+
expect(spawnedTask).not.toContain('{{steps.prepare.output}}');
|
|
714
|
+
expect(Buffer.byteLength(spawnedTask, 'utf8')).toBeLessThan(2048);
|
|
715
|
+
expect(taskFilePath).toBeTruthy();
|
|
716
|
+
expect(Buffer.byteLength(taskFileContents, 'utf8')).toBeGreaterThan(
|
|
717
|
+
WorkflowRunner.PTY_TASK_ARG_SIZE_LIMIT
|
|
718
|
+
);
|
|
719
|
+
expect(taskFileContents).toContain('Review the injected context below');
|
|
720
|
+
expect(existsSync(taskFilePath)).toBe(false);
|
|
721
|
+
} finally {
|
|
722
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
655
726
|
it('should pass canonical bypass args to interactive codex PTY spawns', async () => {
|
|
656
727
|
mockSpawnOutputs = [
|
|
657
728
|
'LEAD_DONE\n',
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('@relaycast/sdk', () => ({
|
|
4
|
+
RelayCast: vi.fn(),
|
|
5
|
+
RelayError: class RelayError extends Error {},
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../relay.js', () => ({
|
|
9
|
+
AgentRelay: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const { WorkflowRunner } = await import('../runner.js');
|
|
13
|
+
|
|
14
|
+
describe('runVerification output_contains (token double-count fix)', () => {
|
|
15
|
+
function createRunner(): InstanceType<typeof WorkflowRunner> {
|
|
16
|
+
return new WorkflowRunner({ cwd: '/tmp/test' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function runVerification(
|
|
20
|
+
runner: InstanceType<typeof WorkflowRunner>,
|
|
21
|
+
check: { type: 'output_contains'; value: string },
|
|
22
|
+
output: string,
|
|
23
|
+
stepName: string,
|
|
24
|
+
injectedTaskText?: string
|
|
25
|
+
) {
|
|
26
|
+
return (runner as any).runVerification(check, output, stepName, injectedTaskText, {
|
|
27
|
+
allowFailure: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
it('passes when token is in output and not in task injection', () => {
|
|
32
|
+
const runner = createRunner();
|
|
33
|
+
const result = runVerification(
|
|
34
|
+
runner,
|
|
35
|
+
{ type: 'output_contains', value: 'DONE' },
|
|
36
|
+
'Task completed. DONE',
|
|
37
|
+
'step1'
|
|
38
|
+
);
|
|
39
|
+
expect(result.passed).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('fails when token is missing from output entirely', () => {
|
|
43
|
+
const runner = createRunner();
|
|
44
|
+
const result = runVerification(
|
|
45
|
+
runner,
|
|
46
|
+
{ type: 'output_contains', value: 'DONE' },
|
|
47
|
+
'Task completed without the marker',
|
|
48
|
+
'step1'
|
|
49
|
+
);
|
|
50
|
+
expect(result.passed).toBe(false);
|
|
51
|
+
expect(result.error).toContain('does not contain "DONE"');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('passes when token is in both task injection and agent output', () => {
|
|
55
|
+
const runner = createRunner();
|
|
56
|
+
const result = runVerification(
|
|
57
|
+
runner,
|
|
58
|
+
{ type: 'output_contains', value: 'REFLECTION_COMPLETE' },
|
|
59
|
+
'Your task: output REFLECTION_COMPLETE when done\n\nI have finished. REFLECTION_COMPLETE',
|
|
60
|
+
'step1',
|
|
61
|
+
'Your task: output REFLECTION_COMPLETE when done'
|
|
62
|
+
);
|
|
63
|
+
expect(result.passed).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('fails when token appears only in task injection (not produced by agent)', () => {
|
|
67
|
+
const runner = createRunner();
|
|
68
|
+
const result = runVerification(
|
|
69
|
+
runner,
|
|
70
|
+
{ type: 'output_contains', value: 'REFLECTION_COMPLETE' },
|
|
71
|
+
'Your task: output REFLECTION_COMPLETE when done\n\nI worked on it but forgot the marker.',
|
|
72
|
+
'step1',
|
|
73
|
+
'Your task: output REFLECTION_COMPLETE when done'
|
|
74
|
+
);
|
|
75
|
+
expect(result.passed).toBe(false);
|
|
76
|
+
expect(result.error).toContain('does not contain "REFLECTION_COMPLETE"');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('handles token appearing multiple times in task injection', () => {
|
|
80
|
+
const runner = createRunner();
|
|
81
|
+
const taskText = 'Output DONE when done. Remember: DONE is required.';
|
|
82
|
+
const output = taskText + '\n\nAll work complete. DONE';
|
|
83
|
+
const result = runVerification(
|
|
84
|
+
runner,
|
|
85
|
+
{ type: 'output_contains', value: 'DONE' },
|
|
86
|
+
output,
|
|
87
|
+
'step1',
|
|
88
|
+
taskText
|
|
89
|
+
);
|
|
90
|
+
expect(result.passed).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('fails when token appears same number of times as in task injection', () => {
|
|
94
|
+
const runner = createRunner();
|
|
95
|
+
const taskText = 'Output DONE when done. Remember: DONE is required.';
|
|
96
|
+
const output = taskText + '\n\nAll work complete but no marker here.';
|
|
97
|
+
const result = runVerification(
|
|
98
|
+
runner,
|
|
99
|
+
{ type: 'output_contains', value: 'DONE' },
|
|
100
|
+
output,
|
|
101
|
+
'step1',
|
|
102
|
+
taskText
|
|
103
|
+
);
|
|
104
|
+
expect(result.passed).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('handles empty token gracefully', () => {
|
|
108
|
+
const runner = createRunner();
|
|
109
|
+
const result = runVerification(
|
|
110
|
+
runner,
|
|
111
|
+
{ type: 'output_contains', value: '' },
|
|
112
|
+
'some output',
|
|
113
|
+
'step1'
|
|
114
|
+
);
|
|
115
|
+
expect(result.passed).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -9,6 +9,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
9
9
|
import {
|
|
10
10
|
createWriteStream,
|
|
11
11
|
existsSync,
|
|
12
|
+
mkdtempSync,
|
|
12
13
|
mkdirSync,
|
|
13
14
|
readFileSync,
|
|
14
15
|
readdirSync,
|
|
@@ -17,7 +18,8 @@ import {
|
|
|
17
18
|
writeFileSync,
|
|
18
19
|
} from 'node:fs';
|
|
19
20
|
import type { Dirent, WriteStream } from 'node:fs';
|
|
20
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
21
|
+
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
21
23
|
import path from 'node:path';
|
|
22
24
|
import chalk from 'chalk';
|
|
23
25
|
|
|
@@ -97,6 +99,7 @@ interface SpawnResult {
|
|
|
97
99
|
output: string;
|
|
98
100
|
exitCode?: number;
|
|
99
101
|
exitSignal?: string;
|
|
102
|
+
promptTaskText?: string;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/** Error carrying exit code/signal from a failed subprocess spawn. */
|
|
@@ -364,6 +367,7 @@ export class WorkflowRunner {
|
|
|
364
367
|
private readonly activeReviewers = new Map<string, number>();
|
|
365
368
|
/** Structured CLI session reports captured during the current run, keyed by step name. */
|
|
366
369
|
private readonly agentReports = new Map<string, CliSessionReport>();
|
|
370
|
+
private static readonly PTY_TASK_ARG_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB
|
|
367
371
|
|
|
368
372
|
constructor(options: WorkflowRunnerOptions = {}) {
|
|
369
373
|
this.db = options.db ?? new InMemoryWorkflowDb();
|
|
@@ -3539,6 +3543,7 @@ export class WorkflowRunner {
|
|
|
3539
3543
|
let ownerOutput: string;
|
|
3540
3544
|
let ownerElapsed: number;
|
|
3541
3545
|
let completionReason: WorkflowStepCompletionReason | undefined;
|
|
3546
|
+
let promptTaskText: string | undefined;
|
|
3542
3547
|
|
|
3543
3548
|
if (usesDedicatedOwner) {
|
|
3544
3549
|
const result = await this.executeSupervisedAgentStep(
|
|
@@ -3592,6 +3597,12 @@ export class WorkflowRunner {
|
|
|
3592
3597
|
: undefined,
|
|
3593
3598
|
});
|
|
3594
3599
|
const output = typeof spawnResult === 'string' ? spawnResult : spawnResult.output;
|
|
3600
|
+
promptTaskText =
|
|
3601
|
+
typeof spawnResult === 'string'
|
|
3602
|
+
? effectiveOwner.interactive === false
|
|
3603
|
+
? undefined
|
|
3604
|
+
: ownerTask
|
|
3605
|
+
: spawnResult.promptTaskText ?? ownerTask;
|
|
3595
3606
|
lastExitCode = typeof spawnResult === 'string' ? undefined : spawnResult.exitCode;
|
|
3596
3607
|
lastExitSignal = typeof spawnResult === 'string' ? undefined : spawnResult.exitSignal;
|
|
3597
3608
|
ownerElapsed = Date.now() - ownerStartTime;
|
|
@@ -3602,8 +3613,8 @@ export class WorkflowRunner {
|
|
|
3602
3613
|
step,
|
|
3603
3614
|
output,
|
|
3604
3615
|
output,
|
|
3605
|
-
ownerTask,
|
|
3606
|
-
|
|
3616
|
+
promptTaskText ?? ownerTask,
|
|
3617
|
+
promptTaskText ?? ownerTask
|
|
3607
3618
|
);
|
|
3608
3619
|
completionReason = completionDecision.completionReason;
|
|
3609
3620
|
} catch (error) {
|
|
@@ -3654,7 +3665,7 @@ export class WorkflowRunner {
|
|
|
3654
3665
|
step.verification,
|
|
3655
3666
|
specialistOutput,
|
|
3656
3667
|
step.name,
|
|
3657
|
-
|
|
3668
|
+
promptTaskText
|
|
3658
3669
|
);
|
|
3659
3670
|
completionReason = verificationResult.completionReason;
|
|
3660
3671
|
}
|
|
@@ -4028,7 +4039,14 @@ export class WorkflowRunner {
|
|
|
4028
4039
|
detail: `Worker ${workerRuntimeName} exited`,
|
|
4029
4040
|
raw: { worker: workerRuntimeName, exitCode: result.exitCode, exitSignal: result.exitSignal },
|
|
4030
4041
|
});
|
|
4031
|
-
if (
|
|
4042
|
+
if (
|
|
4043
|
+
step.verification?.type === 'output_contains' &&
|
|
4044
|
+
this.outputContainsVerificationToken(
|
|
4045
|
+
result.output,
|
|
4046
|
+
step.verification.value,
|
|
4047
|
+
result.promptTaskText
|
|
4048
|
+
)
|
|
4049
|
+
) {
|
|
4032
4050
|
this.log(
|
|
4033
4051
|
`[${step.name}] Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`
|
|
4034
4052
|
);
|
|
@@ -4079,13 +4097,14 @@ export class WorkflowRunner {
|
|
|
4079
4097
|
const ownerElapsed = Date.now() - ownerStartTime;
|
|
4080
4098
|
const ownerOutput = ownerResultObj.output;
|
|
4081
4099
|
this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
|
|
4082
|
-
const
|
|
4100
|
+
const workerResultObj = await workerPromise;
|
|
4101
|
+
const specialistOutput = workerResultObj.output;
|
|
4083
4102
|
const completionDecision = this.resolveOwnerCompletionDecision(
|
|
4084
4103
|
step,
|
|
4085
4104
|
ownerOutput,
|
|
4086
4105
|
specialistOutput,
|
|
4087
|
-
supervisorTask,
|
|
4088
|
-
|
|
4106
|
+
ownerResultObj.promptTaskText ?? supervisorTask,
|
|
4107
|
+
workerResultObj.promptTaskText ?? specialistTask
|
|
4089
4108
|
);
|
|
4090
4109
|
return {
|
|
4091
4110
|
specialistOutput,
|
|
@@ -4359,6 +4378,10 @@ export class WorkflowRunner {
|
|
|
4359
4378
|
injectedTaskText: string
|
|
4360
4379
|
): boolean {
|
|
4361
4380
|
const marker = `STEP_COMPLETE:${step.name}`;
|
|
4381
|
+
const strippedOutput = this.stripInjectedTaskEcho(output, injectedTaskText);
|
|
4382
|
+
if (strippedOutput.includes(marker)) {
|
|
4383
|
+
return true;
|
|
4384
|
+
}
|
|
4362
4385
|
const taskHasMarker = injectedTaskText.includes(marker);
|
|
4363
4386
|
const first = output.indexOf(marker);
|
|
4364
4387
|
if (first === -1) {
|
|
@@ -4448,6 +4471,65 @@ export class WorkflowRunner {
|
|
|
4448
4471
|
.join('\n');
|
|
4449
4472
|
}
|
|
4450
4473
|
|
|
4474
|
+
private stripInjectedTaskEcho(output: string, injectedTaskText?: string): string {
|
|
4475
|
+
if (!injectedTaskText) {
|
|
4476
|
+
return output;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
const candidates = [
|
|
4480
|
+
injectedTaskText,
|
|
4481
|
+
injectedTaskText.replace(/\r\n/g, '\n'),
|
|
4482
|
+
injectedTaskText.replace(/\n/g, '\r\n'),
|
|
4483
|
+
].filter((candidate, index, all) => candidate.length > 0 && all.indexOf(candidate) === index);
|
|
4484
|
+
|
|
4485
|
+
for (const candidate of candidates) {
|
|
4486
|
+
const start = output.indexOf(candidate);
|
|
4487
|
+
if (start !== -1) {
|
|
4488
|
+
return output.slice(0, start) + output.slice(start + candidate.length);
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
return output;
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
private outputContainsVerificationToken(
|
|
4496
|
+
output: string,
|
|
4497
|
+
token: string,
|
|
4498
|
+
injectedTaskText?: string
|
|
4499
|
+
): boolean {
|
|
4500
|
+
if (!token) {
|
|
4501
|
+
return false;
|
|
4502
|
+
}
|
|
4503
|
+
return this.stripInjectedTaskEcho(output, injectedTaskText).includes(token);
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
private prepareInteractiveSpawnTask(
|
|
4507
|
+
agentName: string,
|
|
4508
|
+
taskText: string
|
|
4509
|
+
): { spawnTaskText: string; promptTaskText: string; taskTmpFile?: string } {
|
|
4510
|
+
if (Buffer.byteLength(taskText, 'utf8') <= WorkflowRunner.PTY_TASK_ARG_SIZE_LIMIT) {
|
|
4511
|
+
return {
|
|
4512
|
+
spawnTaskText: taskText,
|
|
4513
|
+
promptTaskText: taskText,
|
|
4514
|
+
};
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
const taskTmpDir = mkdtempSync(path.join(tmpdir(), 'relay-pty-task-'));
|
|
4518
|
+
const taskTmpFile = path.join(taskTmpDir, `${agentName}-${Date.now()}.txt`);
|
|
4519
|
+
writeFileSync(taskTmpFile, taskText, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
4520
|
+
const promptTaskText =
|
|
4521
|
+
`TASK_FILE:${taskTmpFile}\n` +
|
|
4522
|
+
'Read that file completely before taking any action.\n' +
|
|
4523
|
+
'Treat the file contents as the full workflow task and follow them exactly.\n' +
|
|
4524
|
+
'Do not ask for the task again.';
|
|
4525
|
+
|
|
4526
|
+
return {
|
|
4527
|
+
spawnTaskText: promptTaskText,
|
|
4528
|
+
promptTaskText,
|
|
4529
|
+
taskTmpFile,
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4451
4533
|
private firstMeaningfulLine(output: string): string | undefined {
|
|
4452
4534
|
return output
|
|
4453
4535
|
.split('\n')
|
|
@@ -5218,6 +5300,7 @@ export class WorkflowRunner {
|
|
|
5218
5300
|
'(b) outputting the exact text "/exit" on its own line as a fallback. ' +
|
|
5219
5301
|
'Do not wait for further input — terminate immediately after finishing. ' +
|
|
5220
5302
|
'Do NOT spawn sub-agents unless the task explicitly requires it.';
|
|
5303
|
+
const preparedTask = this.prepareInteractiveSpawnTask(agentName, taskWithExit);
|
|
5221
5304
|
|
|
5222
5305
|
// Register PTY output listener before spawning so we capture everything
|
|
5223
5306
|
this.ptyOutputBuffers.set(agentName, []);
|
|
@@ -5257,7 +5340,7 @@ export class WorkflowRunner {
|
|
|
5257
5340
|
model: agentDef.constraints?.model,
|
|
5258
5341
|
args: interactiveSpawnPolicy.args,
|
|
5259
5342
|
channels: agentChannels,
|
|
5260
|
-
task:
|
|
5343
|
+
task: preparedTask.spawnTaskText,
|
|
5261
5344
|
idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
|
|
5262
5345
|
cwd: agentCwd,
|
|
5263
5346
|
});
|
|
@@ -5368,6 +5451,7 @@ export class WorkflowRunner {
|
|
|
5368
5451
|
agentDef,
|
|
5369
5452
|
step,
|
|
5370
5453
|
timeoutMs,
|
|
5454
|
+
preparedTask.promptTaskText,
|
|
5371
5455
|
options.preserveOnIdle ?? this.shouldPreserveIdleSupervisor(agentDef, step, options.evidenceRole)
|
|
5372
5456
|
);
|
|
5373
5457
|
|
|
@@ -5385,7 +5469,7 @@ export class WorkflowRunner {
|
|
|
5385
5469
|
step.verification,
|
|
5386
5470
|
ptyOutput,
|
|
5387
5471
|
step.name,
|
|
5388
|
-
|
|
5472
|
+
preparedTask.promptTaskText,
|
|
5389
5473
|
{ allowFailure: true }
|
|
5390
5474
|
);
|
|
5391
5475
|
if (verificationResult.passed) {
|
|
@@ -5446,6 +5530,9 @@ export class WorkflowRunner {
|
|
|
5446
5530
|
this.unregisterWorker(agentName);
|
|
5447
5531
|
this.supervisedRuntimeAgents.delete(agentName);
|
|
5448
5532
|
this.runtimeStepAgents.delete(agentName);
|
|
5533
|
+
if (preparedTask.taskTmpFile) {
|
|
5534
|
+
await unlink(preparedTask.taskTmpFile).catch(() => undefined);
|
|
5535
|
+
}
|
|
5449
5536
|
}
|
|
5450
5537
|
|
|
5451
5538
|
let output: string;
|
|
@@ -5480,6 +5567,7 @@ export class WorkflowRunner {
|
|
|
5480
5567
|
output,
|
|
5481
5568
|
exitCode: agent?.exitCode,
|
|
5482
5569
|
exitSignal: agent?.exitSignal,
|
|
5570
|
+
promptTaskText: preparedTask.promptTaskText,
|
|
5483
5571
|
};
|
|
5484
5572
|
}
|
|
5485
5573
|
|
|
@@ -5547,6 +5635,7 @@ export class WorkflowRunner {
|
|
|
5547
5635
|
agentDef: AgentDefinition,
|
|
5548
5636
|
step: WorkflowStep,
|
|
5549
5637
|
timeoutMs?: number,
|
|
5638
|
+
promptTaskText?: string,
|
|
5550
5639
|
preserveIdleSupervisor = false
|
|
5551
5640
|
): Promise<'exited' | 'timeout' | 'released' | 'force-released'> {
|
|
5552
5641
|
const nudgeConfig = this.currentConfig?.swarm.idleNudge;
|
|
@@ -5572,21 +5661,14 @@ export class WorkflowRunner {
|
|
|
5572
5661
|
]);
|
|
5573
5662
|
if (result.kind === 'idle' && result.result === 'idle') {
|
|
5574
5663
|
// Check verification before treating idle as complete.
|
|
5575
|
-
// Mirror runVerification's double-occurrence guard: if the task text
|
|
5576
|
-
// contains the token (from the prompt instruction), require a second
|
|
5577
|
-
// occurrence from the agent's actual output to avoid false positives.
|
|
5578
5664
|
if (step.verification && step.verification.type === 'output_contains') {
|
|
5579
5665
|
const token = step.verification.value;
|
|
5580
5666
|
const ptyOutput = (this.ptyOutputBuffers.get(agent.name) ?? []).join('');
|
|
5581
|
-
const
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
verificationPassed = first !== -1 && ptyOutput.includes(token, first + token.length);
|
|
5587
|
-
} else {
|
|
5588
|
-
verificationPassed = ptyOutput.includes(token);
|
|
5589
|
-
}
|
|
5667
|
+
const verificationPassed = this.outputContainsVerificationToken(
|
|
5668
|
+
ptyOutput,
|
|
5669
|
+
token,
|
|
5670
|
+
promptTaskText
|
|
5671
|
+
);
|
|
5590
5672
|
if (!verificationPassed) {
|
|
5591
5673
|
// The broker fires agent_idle only once per idle transition.
|
|
5592
5674
|
// If the agent is still working (will produce output then idle again),
|
|
@@ -5798,23 +5880,8 @@ export class WorkflowRunner {
|
|
|
5798
5880
|
|
|
5799
5881
|
switch (check.type) {
|
|
5800
5882
|
case 'output_contains': {
|
|
5801
|
-
// Guard against false positives: the PTY captures the injected task text
|
|
5802
|
-
// verbatim, so if the verification token appears in the task itself the
|
|
5803
|
-
// check would pass immediately without the agent doing any real work.
|
|
5804
|
-
// When the task contains the token, require a SECOND occurrence — one
|
|
5805
|
-
// from the task injection and one from the agent's actual response.
|
|
5806
5883
|
const token = check.value;
|
|
5807
|
-
|
|
5808
|
-
if (taskHasToken) {
|
|
5809
|
-
const first = output.indexOf(token);
|
|
5810
|
-
const hasSecond = first !== -1 && output.includes(token, first + token.length);
|
|
5811
|
-
if (!hasSecond) {
|
|
5812
|
-
return fail(
|
|
5813
|
-
`Verification failed for "${stepName}": output does not contain "${token}" ` +
|
|
5814
|
-
`(token found only in task injection — agent must output it explicitly)`
|
|
5815
|
-
);
|
|
5816
|
-
}
|
|
5817
|
-
} else if (!output.includes(token)) {
|
|
5884
|
+
if (!this.outputContainsVerificationToken(output, token, injectedTaskText)) {
|
|
5818
5885
|
return fail(`Verification failed for "${stepName}": output does not contain "${token}"`);
|
|
5819
5886
|
}
|
|
5820
5887
|
break;
|
|
@@ -218,6 +218,8 @@ public final class RelayObserver: NSObject, URLSessionWebSocketDelegate, @unchec
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
private func _handleSocketError(_ error: Error) {
|
|
221
|
+
isConnectionReady = false
|
|
222
|
+
|
|
221
223
|
guard reconnectAttempts < maxReconnectAttempts else {
|
|
222
224
|
_connectionState = .disconnected
|
|
223
225
|
let delegate = self.delegate
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/trajectory",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.21",
|
|
4
4
|
"description": "Trajectory integration utilities (trail/PDERO) for Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/config": "3.2.
|
|
25
|
+
"@agent-relay/config": "3.2.21"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/user-directory",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.21",
|
|
4
4
|
"description": "User directory service for agent-relay (per-user credential storage)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/utils": "3.2.
|
|
25
|
+
"@agent-relay/utils": "3.2.21"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/utils",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.21",
|
|
4
4
|
"description": "Shared utilities for agent-relay: logging, name generation, command resolution, update checking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"vitest": "^3.2.4"
|
|
113
113
|
},
|
|
114
114
|
"dependencies": {
|
|
115
|
-
"@agent-relay/config": "3.2.
|
|
115
|
+
"@agent-relay/config": "3.2.21",
|
|
116
116
|
"compare-versions": "^6.1.1"
|
|
117
117
|
},
|
|
118
118
|
"publishConfig": {
|