agents-relay 1.0.30 → 1.0.32
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 +19 -4
- package/dist/adapters.js +17 -29
- package/dist/cli.js +91 -24
- package/dist/dashboard.js +1 -1
- package/dist/execution-context.js +258 -0
- package/dist/planner.js +17 -3
- package/dist/reconciler.js +70 -17
- package/dist/relayd.js +7 -5
- package/dist/store.js +1 -1
- package/dist/workspace.js +11 -0
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +17 -1
- package/skills/chatgpt-browser-worker/SKILL.md +0 -67
- package/skills/chatgpt-browser-worker/agents/browser-worker.agent.md +0 -47
- package/skills/chatgpt-browser-worker/scripts/_submit_readiness.py +0 -30
- package/skills/chatgpt-browser-worker/scripts/_temporary_bh.py +0 -298
- package/skills/chatgpt-browser-worker/scripts/temporary_bh.py +0 -36
package/README.md
CHANGED
|
@@ -31,9 +31,16 @@ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id chil
|
|
|
31
31
|
--capabilities model,chatgpt,browser-harness
|
|
32
32
|
~~~
|
|
33
33
|
|
|
34
|
-
Jobs default to `fixed`, preserving the original submit-and-run behavior.
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
Jobs default to `fixed`, preserving the original submit-and-run behavior. Every
|
|
35
|
+
executable task is now created by a completed durable planning decision. `job
|
|
36
|
+
create --start` records `planner-0` (supplied by the orchestrator) and the
|
|
37
|
+
initial task atomically from the caller's perspective; retrying the command is
|
|
38
|
+
idempotent. Use `--mode autonomous` with `init` or `job create` to make
|
|
39
|
+
reconciliation own planner progress. Autonomous creation with only an objective
|
|
40
|
+
may create a model planner first, but it cannot materialize work until that
|
|
41
|
+
planner succeeds. Fixed jobs never grow another planning round.
|
|
42
|
+
|
|
43
|
+
The durable status JSON and dashboard show the mode and planning provenance. An
|
|
37
44
|
autonomous runtime uses the bundled `skills/agents-relay/agents/planner.agent.md` by default; `--planner-command` is an optional override
|
|
38
45
|
snapshot as JSON on stdin and must return `{ "objective_status": "in_progress"|"satisfied", "assessment": "...", "next_tasks": [...] }`.
|
|
39
46
|
Planner-created tasks use stable IDs and the existing parent/subtask tree, so
|
|
@@ -93,6 +100,14 @@ V1 assumes one active runner/daemon per job. Use a distributed atomic lease befo
|
|
|
93
100
|
|
|
94
101
|
Codex/model-backed tasks require --provider and --model (or equivalent routing metadata) before launch. See [docs/architecture.md](docs/architecture.md) and [skills/agents-relay/SKILL.md](skills/agents-relay/SKILL.md).
|
|
95
102
|
|
|
103
|
+
XChat and other orchestrators can consume Relay's exact shared discovery implementation without duplicating filesystem search logic:
|
|
104
|
+
|
|
105
|
+
~~~sh
|
|
106
|
+
npx agents-relay context resolve --project-path ~/Workspace/agents-relay --role orchestrator
|
|
107
|
+
~~~
|
|
108
|
+
|
|
109
|
+
The command resolves concrete paths only for the current execution. Use `--skills` / `--agents` for explicit selections; no resolved absolute path is written into durable task state.
|
|
110
|
+
|
|
96
111
|
The minimal agent-network surface is machine-driven registration and discovery. `agent-register` persists an agent identity, responsibility boundary, claimed capabilities, endpoint/runtime, availability, and routing metadata in a trusted PR marker; `agent-discover` applies hard filters and returns evidence-backed candidates. Adapters remain runtimes, not agent identities. See [docs/architecture.md](docs/architecture.md) for the constrained future remote submission contract.
|
|
97
112
|
|
|
98
113
|
### Agent worker lifecycle
|
|
@@ -103,7 +118,7 @@ Every model-backed task declares a durable output with `--output task-pr` or `--
|
|
|
103
118
|
|
|
104
119
|
For Codex and ChatGPT, **events are authoritative task state**. Event publication uses the canonical Neo `events-bus` surface (`events__publish` for sandboxed/hosted workers, or `NEO_EVENTS_EMIT` for direct local workers); Agents Relay only subscribes and reconciles. Relay publishes `task.process.launched` only after successful adapter launch; `task.started` is worker-owned and must be a new, correlated start acknowledgement. Codex publishes `task.process.exited` when its synchronous runtime exits, while ChatGPT publishes `task.process.async_exited` after its submission runtime exits; neither substitutes for `task.completed`, `task.failed`, `task.blocked`, or `task.cancelled`. The ChatGPT worker-owned tab follows the configured close policy: `after-start` waits for the correlated start acknowledgement, `never` keeps it open for debug, and `after-terminal` waits for a new exact-task terminal event; start-ack failure closes only the owned tab after 60 seconds. The executing agent must publish exactly one correlated terminal event. Relay persists the matching durable state only from that terminal event. Model-backed tasks require an event bus and time out explicitly if no terminal event arrives.
|
|
105
120
|
|
|
106
|
-
The ChatGPT adapter uses the
|
|
121
|
+
The ChatGPT adapter uses the `chatgpt-browser-worker` path selected by the shared execution-context resolver; Relay does not bundle its own copy. The resolver applies project-local > Neo shared > global `~/.agents` precedence at execution time, so durable tasks store names/intent rather than stale absolute skill paths. It opens a fresh Temporary Chat tab, uses the account defaults, submits the prompt, verifies acceptance, and applies `--chatgpt-tab-close-policy` (or `AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY`): `after-start` by default, `never` for test/debug, or `after-terminal` after a new exact-task terminal event. It never closes a user tab and does not poll for the assistant response, resume/reopen a thread, or use conversation text as the result channel. Any observed ChatGPT thread ID is diagnostic only.
|
|
107
122
|
|
|
108
123
|
~~~sh
|
|
109
124
|
npx agents-relay submit --repo OWNER/REPO --pr 5 --id job-1 \
|
package/dist/adapters.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { access, readFile, writeFile, unlink } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
4
|
import { randomUUID } from 'node:crypto';
|
|
7
5
|
export function buildCodexArgs(route, input) {
|
|
8
6
|
const args = ['exec', '--json'];
|
|
@@ -108,33 +106,23 @@ export class LaunchFailure extends Error {
|
|
|
108
106
|
}
|
|
109
107
|
export class StartAckTimeout extends LaunchFailure {
|
|
110
108
|
}
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
async function loadBrowserWorkerRuntime(explicit) {
|
|
128
|
-
for (const candidate of browserWorkerCandidates(explicit)) {
|
|
129
|
-
try {
|
|
130
|
-
await access(candidate);
|
|
131
|
-
return { definition: await readFile(candidate, 'utf8'), root: dirname(dirname(candidate)) };
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
// Try the next installed skill root.
|
|
135
|
-
}
|
|
109
|
+
async function loadBrowserWorkerRuntime(context, explicit) {
|
|
110
|
+
const resolvedSkill = context?.skills.find(skill => skill.name === 'chatgpt-browser-worker');
|
|
111
|
+
// Once a worker receives a resolved execution context, that context owns
|
|
112
|
+
// skill discovery. The explicit path remains the fallback for direct
|
|
113
|
+
// adapter use, where no shared context has been resolved yet.
|
|
114
|
+
const candidate = resolvedSkill
|
|
115
|
+
? join(dirname(resolvedSkill.path), 'agents', 'browser-worker.agent.md')
|
|
116
|
+
: explicit;
|
|
117
|
+
if (!candidate)
|
|
118
|
+
throw new Error('chatgpt-browser-worker is not visible in the resolved execution context');
|
|
119
|
+
try {
|
|
120
|
+
await access(candidate);
|
|
121
|
+
return { definition: await readFile(candidate, 'utf8'), root: dirname(dirname(candidate)) };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new Error(`chatgpt-browser-worker agent definition not found at resolved path: ${candidate}`);
|
|
136
125
|
}
|
|
137
|
-
throw new Error('chatgpt-browser-worker agent definition not found; set AGENTS_RELAY_CHATGPT_BROWSER_WORKER_AGENT');
|
|
138
126
|
}
|
|
139
127
|
function parseDriverJson(output) {
|
|
140
128
|
const lines = output.trim().split(/\r?\n/).filter(Boolean);
|
|
@@ -260,7 +248,7 @@ export class ChatGptAdapter {
|
|
|
260
248
|
execution.launched = new Promise((resolve, reject) => { launchResolve = resolve; launchReject = reject; });
|
|
261
249
|
execution.launched.catch(() => { });
|
|
262
250
|
execution.promise = (async () => {
|
|
263
|
-
const runtime = await loadBrowserWorkerRuntime(this.agentPath);
|
|
251
|
+
const runtime = await loadBrowserWorkerRuntime(context.executionContext, this.agentPath);
|
|
264
252
|
if (controller.signal.aborted)
|
|
265
253
|
throw new Error('browser-worker execution aborted');
|
|
266
254
|
const policy = chatGptTabClosePolicy(task.chatgptTabClosePolicy, this.tabClosePolicy);
|
package/dist/cli.js
CHANGED
|
@@ -14,9 +14,11 @@ import { NatsEventBus, eventFor } from './events.js';
|
|
|
14
14
|
import { serveDashboard } from './dashboard.js';
|
|
15
15
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
16
|
import { discoverAgents } from './registry.js';
|
|
17
|
-
import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
17
|
+
import { AgentObjectivePlanner, CommandObjectivePlanner, orchestratorPlanningDecision } from './planner.js';
|
|
18
18
|
import { extendRetryBudget } from './scheduler.js';
|
|
19
|
-
|
|
19
|
+
import { resolveExecutionContext } from './execution-context.js';
|
|
20
|
+
import { defaultWorkspaceRoot, resolveManagedProjectPath } from './workspace.js';
|
|
21
|
+
const usage = 'agents-relay <job|task|context|init|submit|record|agent-register|agent-discover|status|reconcile|retry|cancel|serve> [options]';
|
|
20
22
|
const help = {
|
|
21
23
|
root: `Agents Relay coordinates durable asynchronous agent jobs through GitHub pull requests.
|
|
22
24
|
|
|
@@ -26,6 +28,7 @@ Usage:
|
|
|
26
28
|
Commands:
|
|
27
29
|
job create|adopt|repair Create or repair a durable GitHub-backed job
|
|
28
30
|
task update Update mutable durable task fields in place
|
|
31
|
+
context resolve Resolve shared project instructions, skills, agents, and runtimes
|
|
29
32
|
init Create a local demo job or initialize a GitHub job
|
|
30
33
|
submit Queue a task for a managed job
|
|
31
34
|
record Backfill a completed task into durable history
|
|
@@ -96,6 +99,21 @@ Use --clear-routing to remove routing metadata. A model update requires --model;
|
|
|
96
99
|
|
|
97
100
|
Example:
|
|
98
101
|
npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter chatgpt --provider chatgpt --model gpt-5.6-sol --reasoning high`,
|
|
102
|
+
context: `Resolve the shared execution context used by XChat and Relay.
|
|
103
|
+
|
|
104
|
+
Usage:
|
|
105
|
+
npx agents-relay context resolve --project-path PATH --role ROLE [options]
|
|
106
|
+
|
|
107
|
+
Actions:
|
|
108
|
+
resolve Resolve instructions, visible skills/agents, and runtime/delegation metadata`,
|
|
109
|
+
'context resolve': `Resolve shared execution context without creating durable state.
|
|
110
|
+
|
|
111
|
+
Required: --project-path PATH.
|
|
112
|
+
Common: --role orchestrator|planner|reviewer|worker|nested-worker, --skills a,b, --agents a,b.
|
|
113
|
+
Root overrides for testing/configuration: --workspace-root PATH, --neo-root PATH|none, --global-root PATH.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
npx agents-relay context resolve --project-path ~/Workspace/agents-relay --role orchestrator`,
|
|
99
117
|
submit: `Queue a task for a managed job.
|
|
100
118
|
|
|
101
119
|
Required: --repo OWNER/REPO, --pr NUMBER, --id JOB_ID, and --input TEXT.
|
|
@@ -167,6 +185,8 @@ export function helpText(command, action) {
|
|
|
167
185
|
return action ? help[`job ${action}`] ?? help.job : help.job;
|
|
168
186
|
if (command === 'task')
|
|
169
187
|
return action ? help[`task ${action}`] ?? help.task : help.task;
|
|
188
|
+
if (command === 'context')
|
|
189
|
+
return action ? help[`context ${action}`] ?? help.context : help.context;
|
|
170
190
|
return command ? help[command] ?? help.root : help.root;
|
|
171
191
|
}
|
|
172
192
|
export const SERVICE_DEFAULTS = { port: 8787, watchdogMs: 300000, webhookWatchdogMs: 1800000, dashboardRefreshMs: 300000 };
|
|
@@ -250,7 +270,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
250
270
|
if (!(error instanceof Error) || !error.message.includes('has no durable'))
|
|
251
271
|
throw error;
|
|
252
272
|
const now = new Date().toISOString();
|
|
253
|
-
job = { id: options.id, title: options.title || pr.title || 'Agents Relay job', objective: options.objective || pr.body || undefined, description: pr.body || undefined, priority: options.priority ?? 'P2', executionMode: options.executionMode ?? 'fixed', autoMerge: options.autoMerge ?? true, prNumber: pr.number, repository: repositoryName, state: 'OPEN', continuation: options.continuation ?? null, createdAt: now, updatedAt: now, tasks: [] };
|
|
273
|
+
job = { id: options.id, title: options.title || pr.title || 'Agents Relay job', objective: options.objective || pr.body || undefined, description: pr.body || undefined, priority: options.priority ?? 'P2', executionMode: options.executionMode ?? 'fixed', autoMerge: options.autoMerge ?? true, prNumber: pr.number, repository: repositoryName, state: 'OPEN', continuation: options.continuation ?? null, initialPlanning: options.initialTasks?.length ? { status: 'pending', decisionId: 'planner-0' } : undefined, createdAt: now, updatedAt: now, tasks: [] };
|
|
254
274
|
}
|
|
255
275
|
if (!job.description && pr.body)
|
|
256
276
|
job.description = pr.body;
|
|
@@ -266,9 +286,40 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
266
286
|
job.priority = 'P2';
|
|
267
287
|
if (!job.title && pr.title)
|
|
268
288
|
job.title = pr.title;
|
|
269
|
-
|
|
289
|
+
if (options.initialTasks?.length) {
|
|
290
|
+
const decisionId = job.initialPlanning?.decisionId ?? 'planner-0';
|
|
291
|
+
job.initialPlanning = { status: 'pending', decisionId };
|
|
292
|
+
job.updatedAt = new Date().toISOString();
|
|
293
|
+
await store.saveJob(job);
|
|
294
|
+
const decision = orchestratorPlanningDecision(job, options.initialTasks, decisionId, null, 0);
|
|
295
|
+
if (!(await store.load(options.id)).tasks.some(task => task.id === decision.id))
|
|
296
|
+
await store.appendTask(decision);
|
|
297
|
+
for (const original of options.initialTasks) {
|
|
298
|
+
const task = { ...original, parentTaskId: decision.id, planningDecisionId: decision.id, planningSource: 'orchestrator', planningRound: 0 };
|
|
299
|
+
if (!(await store.load(options.id)).tasks.some(item => item.id === task.id))
|
|
300
|
+
await store.appendTask(task);
|
|
301
|
+
}
|
|
302
|
+
job.initialPlanning = { status: 'complete', decisionId };
|
|
303
|
+
job.updatedAt = new Date().toISOString();
|
|
304
|
+
await store.saveJob(job);
|
|
305
|
+
}
|
|
306
|
+
else
|
|
307
|
+
await store.saveJob(job);
|
|
270
308
|
return { job: await store.load(options.id), pr };
|
|
271
309
|
}
|
|
310
|
+
async function appendPlannedTask(store, job, task) {
|
|
311
|
+
const existing = job.tasks.find(item => item.id === task.id);
|
|
312
|
+
if (existing)
|
|
313
|
+
return existing;
|
|
314
|
+
const decisionId = `plan-${task.id}`;
|
|
315
|
+
const decision = orchestratorPlanningDecision(job, [task], decisionId, task.parentTaskId, job.tasks.filter(item => item.kind === 'planner').length, 'orchestrator');
|
|
316
|
+
if (!job.tasks.some(item => item.id === decision.id))
|
|
317
|
+
await store.appendTask(decision);
|
|
318
|
+
const planned = { ...task, parentTaskId: task.parentTaskId ?? null, planningDecisionId: decision.id, planningSource: 'orchestrator', planningRound: decision.planningRound };
|
|
319
|
+
await store.appendTask(planned);
|
|
320
|
+
job.tasks.push(planned, decision);
|
|
321
|
+
return planned;
|
|
322
|
+
}
|
|
272
323
|
function workerTaskFromArgs(job, args, now, requireInput = false) {
|
|
273
324
|
const model = arg(args, '--model');
|
|
274
325
|
const adapterValue = arg(args, '--adapter');
|
|
@@ -390,6 +441,26 @@ async function runTaskCommand(action, args) {
|
|
|
390
441
|
await saveLocal(localFile, job);
|
|
391
442
|
console.log(JSON.stringify(task, null, 2));
|
|
392
443
|
}
|
|
444
|
+
async function runContextCommand(action, args) {
|
|
445
|
+
if (action !== 'resolve')
|
|
446
|
+
throw new Error(usage);
|
|
447
|
+
const projectPath = arg(args, '--project-path');
|
|
448
|
+
if (!projectPath)
|
|
449
|
+
throw new Error('context resolve requires --project-path');
|
|
450
|
+
const roleValue = arg(args, '--role', 'orchestrator');
|
|
451
|
+
const roles = ['orchestrator', 'planner', 'reviewer', 'worker', 'nested-worker'];
|
|
452
|
+
if (!roles.includes(roleValue))
|
|
453
|
+
throw new Error('--role must be orchestrator, planner, reviewer, worker, or nested-worker');
|
|
454
|
+
const neoRootArg = arg(args, '--neo-root');
|
|
455
|
+
const context = await resolveExecutionContext(projectPath, roleValue, {
|
|
456
|
+
workspaceRoot: arg(args, '--workspace-root') || undefined,
|
|
457
|
+
neoRoot: neoRootArg === 'none' ? null : neoRootArg || undefined,
|
|
458
|
+
globalRoot: arg(args, '--global-root') || undefined,
|
|
459
|
+
explicitSkills: arg(args, '--skills').split(',').filter(Boolean),
|
|
460
|
+
explicitAgents: arg(args, '--agents').split(',').filter(Boolean),
|
|
461
|
+
});
|
|
462
|
+
console.log(JSON.stringify(context, null, 2));
|
|
463
|
+
}
|
|
393
464
|
async function runJobCommand(action, args) {
|
|
394
465
|
if (!['create', 'adopt', 'repair'].includes(action))
|
|
395
466
|
throw new Error(usage);
|
|
@@ -398,12 +469,12 @@ async function runJobCommand(action, args) {
|
|
|
398
469
|
if (!id)
|
|
399
470
|
throw new Error('--id is required for managed GitHub jobs');
|
|
400
471
|
const start = hasArg(args, '--start');
|
|
472
|
+
const initialTask = start ? workerTaskFromArgs({ id, title: '', executionMode: 'fixed', prNumber: 0, repository: repo, state: 'OPEN', continuation: null, createdAt: '', updatedAt: '', tasks: [] }, args, new Date().toISOString(), true) : undefined;
|
|
401
473
|
if (start && action !== 'create')
|
|
402
474
|
throw new Error('--start is only supported by job create');
|
|
403
475
|
if (start) {
|
|
404
476
|
if (!hasArg(args, '--adapter'))
|
|
405
477
|
throw new Error('job create --start requires --adapter codex|chatgpt');
|
|
406
|
-
workerTaskFromArgs({ id, title: '', executionMode: 'fixed', prNumber: 0, repository: repo, state: 'OPEN', continuation: null, createdAt: '', updatedAt: '', tasks: [] }, args, new Date().toISOString(), true);
|
|
407
478
|
}
|
|
408
479
|
const auth = await githubAuthContext(args);
|
|
409
480
|
const client = auth.client;
|
|
@@ -425,23 +496,16 @@ async function runJobCommand(action, args) {
|
|
|
425
496
|
prNumber,
|
|
426
497
|
head: arg(args, '--head') || undefined,
|
|
427
498
|
base: arg(args, '--base', 'main'),
|
|
428
|
-
body: arg(args, '--body') || undefined
|
|
499
|
+
body: arg(args, '--body') || undefined,
|
|
500
|
+
initialTasks: initialTask ? [initialTask] : undefined
|
|
429
501
|
});
|
|
430
|
-
if (start)
|
|
431
|
-
|
|
432
|
-
const existing = result.job.tasks.find(item => item.id === task.id);
|
|
433
|
-
const store = new GitHubStore(client, repo, result.pr.number, trustedAuthors);
|
|
434
|
-
await store.appendTask(task);
|
|
435
|
-
if (!existing) {
|
|
436
|
-
await publishWake(eventBus(args), result.job, task.id, `Task ${task.id} submitted`);
|
|
437
|
-
result.job.tasks.push(task);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
502
|
+
if (start)
|
|
503
|
+
await publishWake(eventBus(args), result.job, initialTask?.id ?? 'initial', `Initial planning decision and task ${initialTask?.id ?? 'initial'} recorded`);
|
|
440
504
|
console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state, task: start ? arg(args, '--task-id', 'initial') : undefined }, null, 2));
|
|
441
505
|
}
|
|
442
506
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
443
507
|
export function runtimeReviewer() { return { adapter: 'chatgpt', routing: { provider: 'chatgpt', model: 'account-defaults', decidedBy: 'fixed-chatgpt-reviewer', decidedAt: new Date().toISOString() } }; }
|
|
444
|
-
export function runtime(store, args, bus) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, reviewer: runtimeReviewer(), eventBus: bus, emit, chatgptTabClosePolicy: policy }); }
|
|
508
|
+
export function runtime(store, args, bus) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, reviewer: runtimeReviewer(), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: job => resolveManagedProjectPath(job.repository, arg(args, '--project-path'), arg(args, '--workspace-root') || defaultWorkspaceRoot()) }); }
|
|
445
509
|
export async function createService(args) {
|
|
446
510
|
const loaded = await storeFor(args);
|
|
447
511
|
const upstream = eventBus(args);
|
|
@@ -519,7 +583,7 @@ async function main() {
|
|
|
519
583
|
return;
|
|
520
584
|
}
|
|
521
585
|
if (args.includes('--help') || args.includes('-h')) {
|
|
522
|
-
console.log(helpText(command, command === 'job' || command === 'task' ? args[0] : undefined));
|
|
586
|
+
console.log(helpText(command, command === 'job' || command === 'task' || command === 'context' ? args[0] : undefined));
|
|
523
587
|
return;
|
|
524
588
|
}
|
|
525
589
|
if (command === 'job') {
|
|
@@ -532,6 +596,11 @@ async function main() {
|
|
|
532
596
|
await runTaskCommand(action ?? '', taskArgs);
|
|
533
597
|
return;
|
|
534
598
|
}
|
|
599
|
+
if (command === 'context') {
|
|
600
|
+
const [action, ...contextArgs] = args;
|
|
601
|
+
await runContextCommand(action ?? '', contextArgs);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
535
604
|
if (command === 'init') {
|
|
536
605
|
const now = new Date().toISOString();
|
|
537
606
|
const job = { id: arg(args, '--id', randomUUID()), title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), executionMode: executionMode(args), autoMerge: !hasArg(args, '--no-auto-merge'), prNumber: Number(arg(args, '--pr', '0')), repository: arg(args, '--repo'), state: 'OPEN', continuation: continuation(args), createdAt: now, updatedAt: now, tasks: [] };
|
|
@@ -576,8 +645,7 @@ async function main() {
|
|
|
576
645
|
throw new Error('record requires --summary');
|
|
577
646
|
const commit = arg(args, '--commit');
|
|
578
647
|
const task = { jobId: job.id, id, projectName: arg(args, '--project') || undefined, agentName: arg(args, '--agent', 'orchestrator'), parentTaskId: arg(args, '--parent') || null, dependencies: arg(args, '--deps').split(',').filter(Boolean), capabilities: [], adapter: 'orchestrator', input: arg(args, '--input', summary), continuation: null, continuationDeliveredAt: null, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: commit || null, threadId: null, result: { summary, data: commit ? { commit } : undefined }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now };
|
|
579
|
-
await store
|
|
580
|
-
job.tasks.push(task);
|
|
648
|
+
await appendPlannedTask(store, job, task);
|
|
581
649
|
await saveLocal(localFile, job);
|
|
582
650
|
console.log(`Recorded ${id}`);
|
|
583
651
|
return;
|
|
@@ -586,16 +654,15 @@ async function main() {
|
|
|
586
654
|
const now = new Date().toISOString();
|
|
587
655
|
const task = workerTaskFromArgs(job, args, now);
|
|
588
656
|
task.id = arg(args, '--task-id', randomUUID());
|
|
589
|
-
await store
|
|
590
|
-
job.tasks.push(task);
|
|
657
|
+
const planned = await appendPlannedTask(store, job, task);
|
|
591
658
|
if (job.state !== 'RUNNING') {
|
|
592
659
|
job.state = 'RUNNING';
|
|
593
660
|
job.updatedAt = now;
|
|
594
661
|
await store.saveJob(job);
|
|
595
662
|
}
|
|
596
663
|
await saveLocal(localFile, job);
|
|
597
|
-
await publishWake(bus, job,
|
|
598
|
-
console.log(`Submitted ${
|
|
664
|
+
await publishWake(bus, job, planned.id, `Planning decision recorded and task ${planned.id} submitted`);
|
|
665
|
+
console.log(`Submitted ${planned.id}`);
|
|
599
666
|
return;
|
|
600
667
|
}
|
|
601
668
|
if (command === 'status') {
|
package/dist/dashboard.js
CHANGED
|
@@ -240,7 +240,7 @@ function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();
|
|
|
240
240
|
function taskNode(task,children,depth){const li=element('li');const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{selectedTaskId=task.id;eventTaskId=task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const id=element('span','task-id',text(task.id));id.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(id,element('span','badge '+value(task.state),text(task.state)));li.append(row);const nested=children.get(task.id)||[];if(nested.length){const ul=element('ul','tree');for(const child of nested)ul.append(taskNode(child,children,depth+1));li.append(ul);}return li;}
|
|
241
241
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const known=new Set(tasks.map(task=>task.id));const children=new Map();const roots=[];for(const task of tasks){if(!task.parentTaskId||!known.has(task.parentTaskId))roots.push(task);else{const siblings=children.get(task.parentTaskId)||[];siblings.push(task);children.set(task.parentTaskId,siblings);}}for(const task of roots)taskTree.append(taskNode(task,children,0));}
|
|
242
242
|
function attemptBudget(task){const attempt=Number(task?.attempt)||0;const max=Number(task?.maxAttempts)||0;if(max>=3&&max%3===0){const effectiveMax=Math.max(max,Math.ceil(attempt/3)*3);return attempt+' / 3×'+(effectiveMax/3);}return attempt+' / '+max;}
|
|
243
|
-
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Task',task.id),field('Kind',task.kind||'work'),field('State',task.state),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),chatField(task));if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));taskDetailRoot.append(groups);}
|
|
243
|
+
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Task',task.id),field('Kind',task.kind||'work'),field('State',task.state),field('Planning decision',task.planningDecisionId||'—'),field('Planning source',task.planningSource||'—'),field('Planning round',task.planningRound===undefined?'—':String(task.planningRound)),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),chatField(task));if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));taskDetailRoot.append(groups);}
|
|
244
244
|
function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}
|
|
245
245
|
function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent='— '+value(job.title||job.id);if(job.id!==selectedJob){selectedJob=job.id;selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('GitHub state',job.githubState||'UNKNOWN'),field('Durable state',job.state),field('Execution mode',job.executionMode||'fixed'),field('Status',awaitingInitialTask(job)?'Awaiting initial task':job.state),field('Review',reviewStatus(job)),field('Review head SHA',job.review?.headSha||job.headSha||'—'),field('Objective',job.objective||job.description||job.title),prField(job),field('Repository',job.repository||'—'),field('Last activity',relativeTime(job.updatedAt)),field('Task states',compactCounts(job.tasks)));jobDetail.append(summary);const selectedTask=(job.tasks||[]).find(task=>task.id===selectedTaskId)||null;if(selectedTaskId&&!selectedTask){selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}buildTree(job.tasks||[]);renderTaskDetail(selectedTask);renderEventScope();}
|
|
246
246
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { access, readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
async function exists(path) {
|
|
5
|
+
try {
|
|
6
|
+
await access(path);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function isWithin(path, root) {
|
|
14
|
+
const value = relative(root, path);
|
|
15
|
+
return value === '' || (!value.startsWith('..' + sep) && value !== '..' && !isAbsolute(value));
|
|
16
|
+
}
|
|
17
|
+
async function directory(path) {
|
|
18
|
+
try {
|
|
19
|
+
return (await stat(path)).isDirectory();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function configuredWorkspaceRoot(projectPath, explicit) {
|
|
26
|
+
const configured = explicit ?? process.env.AGENTS_RELAY_WORKSPACE_ROOT ?? process.env.XCHAT_WORKSPACE_ROOT;
|
|
27
|
+
if (configured)
|
|
28
|
+
return resolve(configured);
|
|
29
|
+
const conventional = join(homedir(), 'Workspace');
|
|
30
|
+
return isWithin(projectPath, conventional) ? conventional : dirname(projectPath);
|
|
31
|
+
}
|
|
32
|
+
async function configuredNeoRoot(workspaceRoot, explicit) {
|
|
33
|
+
if (explicit === null)
|
|
34
|
+
return null;
|
|
35
|
+
const configured = explicit ?? process.env.NEO_SHARED_ROOT;
|
|
36
|
+
if (configured)
|
|
37
|
+
return await directory(resolve(configured)) ? await realpath(resolve(configured)) : null;
|
|
38
|
+
const candidate = join(workspaceRoot, 'neo');
|
|
39
|
+
return await directory(candidate) ? await realpath(candidate) : null;
|
|
40
|
+
}
|
|
41
|
+
function configuredGlobalRoot(explicit) {
|
|
42
|
+
return resolve(explicit ?? process.env.AGENTS_HOME ?? join(homedir(), '.agents'));
|
|
43
|
+
}
|
|
44
|
+
function parseFrontmatter(content) {
|
|
45
|
+
if (!content.startsWith('---\n'))
|
|
46
|
+
return {};
|
|
47
|
+
const end = content.indexOf('\n---', 4);
|
|
48
|
+
if (end < 0)
|
|
49
|
+
return {};
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const line of content.slice(4, end).split(/\r?\n/)) {
|
|
52
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
53
|
+
if (!match)
|
|
54
|
+
continue;
|
|
55
|
+
result[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, '');
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function listValue(value) {
|
|
60
|
+
if (!value)
|
|
61
|
+
return [];
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
const body = trimmed.startsWith('[') && trimmed.endsWith(']') ? trimmed.slice(1, -1) : trimmed;
|
|
64
|
+
return body.split(/[|,]/).map(item => item.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
async function metadata(path) {
|
|
67
|
+
const content = await readFile(path, 'utf8');
|
|
68
|
+
return { fields: parseFrontmatter(content), content };
|
|
69
|
+
}
|
|
70
|
+
async function instructionPaths(projectPath, workspaceRoot, neoRoot, globalRoot) {
|
|
71
|
+
const found = [];
|
|
72
|
+
const add = async (path, source) => {
|
|
73
|
+
if (await exists(path)) {
|
|
74
|
+
const concrete = await realpath(path);
|
|
75
|
+
if (!found.some(item => item.path === concrete))
|
|
76
|
+
found.push({ path: concrete, source });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
await add(join(globalRoot, 'AGENTS.md'), 'global');
|
|
80
|
+
if (isWithin(projectPath, workspaceRoot)) {
|
|
81
|
+
const segments = relative(workspaceRoot, projectPath).split(sep).filter(Boolean);
|
|
82
|
+
let cursor = workspaceRoot;
|
|
83
|
+
await add(join(cursor, 'AGENTS.md'), neoRoot && isWithin(cursor, neoRoot) ? 'neo' : 'project');
|
|
84
|
+
for (const segment of segments) {
|
|
85
|
+
cursor = join(cursor, segment);
|
|
86
|
+
const source = neoRoot && isWithin(cursor, neoRoot) && !isWithin(cursor, projectPath) ? 'neo' : 'project';
|
|
87
|
+
await add(join(cursor, 'AGENTS.md'), source);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
await add(join(projectPath, 'AGENTS.md'), 'project');
|
|
92
|
+
}
|
|
93
|
+
await add(join(projectPath, 'README.md'), 'project');
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
async function skillLayer(root, source) {
|
|
97
|
+
const skillsRoot = join(root, 'skills');
|
|
98
|
+
let entries;
|
|
99
|
+
try {
|
|
100
|
+
entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const result = [];
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (!entry.isDirectory())
|
|
108
|
+
continue;
|
|
109
|
+
const path = join(skillsRoot, entry.name, 'SKILL.md');
|
|
110
|
+
if (!await exists(path))
|
|
111
|
+
continue;
|
|
112
|
+
const { fields } = await metadata(path);
|
|
113
|
+
result.push({ name: fields.name || entry.name, path: await realpath(path), source, description: fields.description || undefined });
|
|
114
|
+
}
|
|
115
|
+
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
116
|
+
}
|
|
117
|
+
async function agentFiles(root, source, skills) {
|
|
118
|
+
const candidates = [];
|
|
119
|
+
const skillDirs = skills.filter(skill => skill.source === source).map(skill => dirname(skill.path));
|
|
120
|
+
for (const skillDir of skillDirs) {
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = await readdir(join(skillDir, 'agents'), { withFileTypes: true });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const entry of entries)
|
|
129
|
+
if (entry.isFile() && entry.name.endsWith('.md'))
|
|
130
|
+
candidates.push(join(skillDir, 'agents', entry.name));
|
|
131
|
+
}
|
|
132
|
+
let direct;
|
|
133
|
+
try {
|
|
134
|
+
direct = await readdir(join(root, 'agents'), { withFileTypes: true });
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
direct = [];
|
|
138
|
+
}
|
|
139
|
+
for (const entry of direct)
|
|
140
|
+
if (entry.isFile() && entry.name.endsWith('.md'))
|
|
141
|
+
candidates.push(join(root, 'agents', entry.name));
|
|
142
|
+
const result = [];
|
|
143
|
+
for (const path of candidates) {
|
|
144
|
+
const { fields } = await metadata(path);
|
|
145
|
+
result.push({
|
|
146
|
+
name: fields.name || basename(path).replace(/\.agent\.md$/i, '').replace(/\.md$/i, ''),
|
|
147
|
+
path: await realpath(path),
|
|
148
|
+
source,
|
|
149
|
+
description: fields.description || undefined,
|
|
150
|
+
capabilities: listValue(fields.capabilities),
|
|
151
|
+
runtime: fields.runtime || listValue(fields.runtimes)[0] || undefined,
|
|
152
|
+
trustDomains: listValue(fields.trustDomains ?? fields.trust_domains ?? fields.trust),
|
|
153
|
+
delegation: listValue(fields.delegation),
|
|
154
|
+
restrictions: listValue(fields.restrictions ?? fields.boundaries),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
function selectByName(items, explicit, kind) {
|
|
160
|
+
if (!explicit?.length)
|
|
161
|
+
return [...items];
|
|
162
|
+
const selected = explicit.map(name => {
|
|
163
|
+
const item = items.find(candidate => candidate.name === name);
|
|
164
|
+
if (!item)
|
|
165
|
+
throw new Error(`Explicit ${kind} ${name} is not visible in the resolved execution context`);
|
|
166
|
+
return item;
|
|
167
|
+
});
|
|
168
|
+
return selected;
|
|
169
|
+
}
|
|
170
|
+
export async function resolveExecutionContext(projectPath, role, options = {}) {
|
|
171
|
+
if (!projectPath)
|
|
172
|
+
throw new Error('projectPath is required');
|
|
173
|
+
const project = await realpath(resolve(projectPath)).catch(() => { throw new Error(`Project path not found: ${projectPath}`); });
|
|
174
|
+
if (!await directory(project))
|
|
175
|
+
throw new Error(`Project path is not a directory: ${projectPath}`);
|
|
176
|
+
const workspaceCandidate = configuredWorkspaceRoot(project, options.workspaceRoot);
|
|
177
|
+
const workspaceRoot = await realpath(workspaceCandidate).catch(() => workspaceCandidate);
|
|
178
|
+
const neoRoot = await configuredNeoRoot(workspaceRoot, options.neoRoot);
|
|
179
|
+
const globalCandidate = configuredGlobalRoot(options.globalRoot);
|
|
180
|
+
const globalRoot = await realpath(globalCandidate).catch(() => globalCandidate);
|
|
181
|
+
const layerRoots = [
|
|
182
|
+
{ root: globalRoot, source: 'global' },
|
|
183
|
+
...(neoRoot ? [{ root: neoRoot, source: 'neo' }] : []),
|
|
184
|
+
{ root: project, source: 'project' },
|
|
185
|
+
];
|
|
186
|
+
const uniqueLayers = layerRoots.filter((layer, index, list) => list.findIndex(item => resolve(item.root) === resolve(layer.root)) === index);
|
|
187
|
+
const skillMap = new Map();
|
|
188
|
+
const skillsByLayer = new Map();
|
|
189
|
+
for (const layer of uniqueLayers) {
|
|
190
|
+
const layerSkills = await skillLayer(layer.root, layer.source);
|
|
191
|
+
skillsByLayer.set(layer.root, layerSkills);
|
|
192
|
+
for (const skill of layerSkills)
|
|
193
|
+
skillMap.set(skill.name, skill);
|
|
194
|
+
}
|
|
195
|
+
const skills = selectByName([...skillMap.values()].sort((a, b) => a.name.localeCompare(b.name)), options.explicitSkills, 'skill');
|
|
196
|
+
const agentMap = new Map();
|
|
197
|
+
for (const layer of uniqueLayers) {
|
|
198
|
+
for (const agent of await agentFiles(layer.root, layer.source, skillsByLayer.get(layer.root) ?? []))
|
|
199
|
+
agentMap.set(agent.name, agent);
|
|
200
|
+
}
|
|
201
|
+
for (const registration of options.registeredAgents ?? []) {
|
|
202
|
+
if (agentMap.has(registration.name))
|
|
203
|
+
continue;
|
|
204
|
+
agentMap.set(registration.name, {
|
|
205
|
+
name: registration.name,
|
|
206
|
+
source: 'registry',
|
|
207
|
+
description: registration.responsibility,
|
|
208
|
+
capabilities: [...registration.capabilities],
|
|
209
|
+
runtime: registration.runtime.adapter,
|
|
210
|
+
trustDomains: [...(registration.routing.trustDomains ?? [])],
|
|
211
|
+
delegation: [],
|
|
212
|
+
restrictions: [...registration.boundaries],
|
|
213
|
+
registration,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const agents = selectByName([...agentMap.values()].sort((a, b) => a.name.localeCompare(b.name)), options.explicitAgents, 'agent');
|
|
217
|
+
return {
|
|
218
|
+
project: { name: basename(project), path: project, workspaceRoot, neoRoot, globalRoot },
|
|
219
|
+
role,
|
|
220
|
+
instructions: await instructionPaths(project, workspaceRoot, neoRoot, globalRoot),
|
|
221
|
+
skills,
|
|
222
|
+
agents,
|
|
223
|
+
runtimes: (options.runtimes ?? []).map(runtime => ({ name: runtime.name ?? runtime.adapter ?? 'unknown', id: runtime.id, capabilities: [...runtime.capabilities], source: 'runtime' })),
|
|
224
|
+
delegation: { mode: 'relay-child-task', parentTaskIdRequired: role === 'nested-worker' },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export function executionContextPrompt(context) {
|
|
228
|
+
const instructions = context.instructions.map(item => `- ${item.path} [${item.source}]`).join('\n') || '- none';
|
|
229
|
+
const skills = context.skills.map(item => `- ${item.name}: ${item.path} [${item.source}]`).join('\n') || '- none';
|
|
230
|
+
const agents = context.agents.map(item => {
|
|
231
|
+
const details = [
|
|
232
|
+
item.capabilities.length ? `capabilities=${item.capabilities.join(',')}` : '',
|
|
233
|
+
item.runtime ? `runtime=${item.runtime}` : '',
|
|
234
|
+
item.trustDomains.length ? `trust=${item.trustDomains.join(',')}` : '',
|
|
235
|
+
item.restrictions.length ? `restrictions=${item.restrictions.join(' | ')}` : '',
|
|
236
|
+
].filter(Boolean).join('; ');
|
|
237
|
+
return `- ${item.name}${item.path ? `: ${item.path}` : ''} [${item.source}]${details ? ` — ${details}` : ''}`;
|
|
238
|
+
}).join('\n') || '- none';
|
|
239
|
+
const runtimes = context.runtimes.map(item => `- ${item.name}${item.id ? ` (${item.id})` : ''}: ${item.capabilities.join(',')}`).join('\n') || '- none';
|
|
240
|
+
return `[Resolved execution context]
|
|
241
|
+
Role: ${context.role}
|
|
242
|
+
Project path: ${context.project.path}
|
|
243
|
+
|
|
244
|
+
Instructions (read in listed order; later/project-local instructions override earlier shared defaults):
|
|
245
|
+
${instructions}
|
|
246
|
+
|
|
247
|
+
Visible skills (already de-duplicated with project-local > Neo shared > global precedence):
|
|
248
|
+
${skills}
|
|
249
|
+
|
|
250
|
+
Visible agents:
|
|
251
|
+
${agents}
|
|
252
|
+
|
|
253
|
+
Runtime capabilities:
|
|
254
|
+
${runtimes}
|
|
255
|
+
|
|
256
|
+
Delegation contract:
|
|
257
|
+
Nested work MUST be created as an Agents Relay durable child task with parentTaskId lineage. Do not directly spawn arbitrary child agents. Leaf workers do not inherit top-level XChat orchestration duties merely because xchat-orchestrator is visible.`;
|
|
258
|
+
}
|