agents-relay 1.0.3 → 1.0.4

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 CHANGED
@@ -17,6 +17,14 @@ npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1
17
17
  npx agents-relay status --repo OWNER/REPO --pr 12 --id job-1
18
18
  ~~~
19
19
 
20
+ Mutable queued/retryable task fields can be changed in place without replacing the durable task identity. `task update` rejects `RUNNING`, `SUCCEEDED`, and `CANCELLED` tasks so active execution and terminal history cannot be rewritten. For example:
21
+
22
+ ~~~sh
23
+ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id child \
24
+ --adapter chatgpt --provider chatgpt --model gpt-5.6-sol --reasoning high \
25
+ --capabilities model,chatgpt,browser-harness
26
+ ~~~
27
+
20
28
  Jobs default to `fixed`, preserving the original submit-and-run behavior. Use
21
29
  `--mode autonomous` with `init` or `job create` to make reconciliation own
22
30
  planner progress. The durable status JSON and dashboard show the mode. An
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import { codexAndZaiUsageRegistry } from './usage.js';
15
15
  import { discoverAgents } from './registry.js';
16
16
  import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
17
17
  import { extendRetryBudget } from './scheduler.js';
18
- const usage = 'agents-relay <job|init|submit|record|agent-register|agent-discover|status|reconcile|retry|cancel|serve> [options]';
18
+ const usage = 'agents-relay <job|task|init|submit|record|agent-register|agent-discover|status|reconcile|retry|cancel|serve> [options]';
19
19
  const help = {
20
20
  root: `Agents Relay coordinates durable asynchronous agent jobs through GitHub pull requests.
21
21
 
@@ -24,6 +24,7 @@ Usage:
24
24
 
25
25
  Commands:
26
26
  job create|adopt|repair Create or repair a durable GitHub-backed job
27
+ task update Update mutable durable task fields in place
27
28
  init Create a local demo job or initialize a GitHub job
28
29
  submit Queue a task for a managed job
29
30
  record Backfill a completed task into durable history
@@ -76,6 +77,21 @@ Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
76
77
 
77
78
  Example:
78
79
  npx agents-relay job repair --repo OWNER/REPO --pr 12 --id job-12`,
80
+ task: `Manage a durable task.
81
+
82
+ Usage:
83
+ npx agents-relay task update [options]
84
+
85
+ Actions:
86
+ update Update mutable task fields without changing task identity/history`,
87
+ 'task update': `Update mutable durable task fields in place.
88
+
89
+ Required: --task-id ID and the normal job storage options.
90
+ Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project.
91
+ Use --clear-routing to remove routing metadata. A model update requires --model; provider defaults to the existing provider or openai.
92
+
93
+ Example:
94
+ 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`,
79
95
  submit: `Queue a task for a managed job.
80
96
 
81
97
  Required: --repo OWNER/REPO, --pr NUMBER, --id JOB_ID, and --input TEXT.
@@ -145,6 +161,8 @@ Example:
145
161
  export function helpText(command, action) {
146
162
  if (command === 'job')
147
163
  return action ? help[`job ${action}`] ?? help.job : help.job;
164
+ if (command === 'task')
165
+ return action ? help[`task ${action}`] ?? help.task : help.task;
148
166
  return command ? help[command] ?? help.root : help.root;
149
167
  }
150
168
  export const SERVICE_DEFAULTS = { port: 8787, watchdogMs: 300000, webhookWatchdogMs: 1800000, dashboardRefreshMs: 300000 };
@@ -172,6 +190,10 @@ class ServiceEventBus {
172
190
  }
173
191
  }
174
192
  function arg(args, name, fallback = '') { const index = args.indexOf(name); return index >= 0 ? args[index + 1] ?? fallback : fallback; }
193
+ function hasArg(args, name) { return args.includes(name); }
194
+ function integerArg(args, name, fallback) { if (!hasArg(args, name))
195
+ return fallback; const value = Number(arg(args, name)); if (!Number.isInteger(value) || value < 0)
196
+ throw new Error(`${name} must be a non-negative integer`); return value; }
175
197
  function repository(args) { const value = arg(args, '--repo'); if (!value)
176
198
  throw new Error('--repo is required for GitHub-backed operation'); return value; }
177
199
  function pullRequest(args) { const value = Number(arg(args, '--pr')); if (!Number.isInteger(value) || value <= 0)
@@ -233,6 +255,66 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
233
255
  await store.saveJob(job);
234
256
  return { job: await store.load(options.id), pr };
235
257
  }
258
+ async function runTaskCommand(action, args) {
259
+ if (action !== 'update')
260
+ throw new Error(usage);
261
+ const { store, job, localFile } = await storeFor(args);
262
+ const id = arg(args, '--task-id');
263
+ if (!id)
264
+ throw new Error('task update requires --task-id');
265
+ const task = job.tasks.find(item => item.id === id);
266
+ if (!task)
267
+ throw new Error(`Task ${id} not found`);
268
+ if (['RUNNING', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
269
+ throw new Error(`Task ${id} is ${task.state}; durable execution fields are immutable in this state`);
270
+ if (hasArg(args, '--adapter')) {
271
+ const adapter = arg(args, '--adapter');
272
+ if (!['shell', 'codex', 'chatgpt', 'orchestrator'].includes(adapter))
273
+ throw new Error('--adapter must be shell, codex, chatgpt, or orchestrator');
274
+ task.adapter = adapter;
275
+ }
276
+ if (hasArg(args, '--priority'))
277
+ task.priority = priority(args, task.priority ?? job.priority ?? 'P2');
278
+ if (hasArg(args, '--input'))
279
+ task.input = arg(args, '--input');
280
+ if (hasArg(args, '--project'))
281
+ task.projectName = arg(args, '--project') || undefined;
282
+ if (hasArg(args, '--agent'))
283
+ task.agentName = arg(args, '--agent') || undefined;
284
+ if (hasArg(args, '--parent'))
285
+ task.parentTaskId = arg(args, '--parent') || null;
286
+ if (hasArg(args, '--deps'))
287
+ task.dependencies = arg(args, '--deps').split(',').filter(Boolean);
288
+ if (hasArg(args, '--capabilities'))
289
+ task.capabilities = arg(args, '--capabilities').split(',').filter(Boolean);
290
+ task.timeoutMs = integerArg(args, '--timeout', task.timeoutMs);
291
+ task.maxAttempts = integerArg(args, '--max-attempts', task.maxAttempts);
292
+ if (hasArg(args, '--max-attempts') && task.maxAttempts < task.attempt)
293
+ throw new Error('--max-attempts cannot be less than the current attempt count');
294
+ const routingFlags = ['--provider', '--model', '--profile', '--reasoning', '--cwd', '--chatgpt-project'];
295
+ if (args.includes('--clear-routing'))
296
+ task.routing = undefined;
297
+ else if (routingFlags.some(flag => hasArg(args, flag))) {
298
+ const current = task.routing;
299
+ const model = hasArg(args, '--model') ? arg(args, '--model') : current?.model;
300
+ if (!model)
301
+ throw new Error('task routing update requires --model when no existing routing is present');
302
+ task.routing = {
303
+ provider: hasArg(args, '--provider') ? arg(args, '--provider') : current?.provider ?? 'openai',
304
+ model,
305
+ profile: hasArg(args, '--profile') ? arg(args, '--profile') || undefined : current?.profile,
306
+ reasoning: hasArg(args, '--reasoning') ? arg(args, '--reasoning') || undefined : current?.reasoning,
307
+ cwd: hasArg(args, '--cwd') ? arg(args, '--cwd') || undefined : current?.cwd,
308
+ projectId: hasArg(args, '--chatgpt-project') ? arg(args, '--chatgpt-project') || undefined : current?.projectId,
309
+ decidedBy: 'cli:update',
310
+ decidedAt: new Date().toISOString()
311
+ };
312
+ }
313
+ task.updatedAt = new Date().toISOString();
314
+ await store.saveTask(task);
315
+ await saveLocal(localFile, job);
316
+ console.log(JSON.stringify(task, null, 2));
317
+ }
236
318
  async function runJobCommand(action, args) {
237
319
  if (!['create', 'adopt', 'repair'].includes(action))
238
320
  throw new Error(usage);
@@ -342,7 +424,7 @@ async function main() {
342
424
  return;
343
425
  }
344
426
  if (args.includes('--help') || args.includes('-h')) {
345
- console.log(helpText(command, command === 'job' ? args[0] : undefined));
427
+ console.log(helpText(command, command === 'job' || command === 'task' ? args[0] : undefined));
346
428
  return;
347
429
  }
348
430
  if (command === 'job') {
@@ -350,6 +432,11 @@ async function main() {
350
432
  await runJobCommand(action ?? '', jobArgs);
351
433
  return;
352
434
  }
435
+ if (command === 'task') {
436
+ const [action, ...taskArgs] = args;
437
+ await runTaskCommand(action ?? '', taskArgs);
438
+ return;
439
+ }
353
440
  if (command === 'init') {
354
441
  const now = new Date().toISOString();
355
442
  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), prNumber: Number(arg(args, '--pr', '0')), repository: arg(args, '--repo'), state: 'OPEN', continuation: continuation(args), createdAt: now, updatedAt: now, tasks: [] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,6 +62,8 @@ Use --file PATH only for explicit local demo/test mode. Optional --events nats e
62
62
 
63
63
  Use task-level continuation when the sender needs to resume, otherwise the job continuation. Continuation delivery is deduplicated by the durable delivery timestamp. Treat BLOCKED as an approval/manual-release state until an explicit retry/release changes it. Cancellation, timeout, and lease expiry are durable state transitions; do not claim success from a worker process exit alone.
64
64
 
65
+ Use `npx agents-relay task update` to change safe durable fields of a `QUEUED`, `READY`, `WAITING`, `BLOCKED`, or `FAILED` task without changing its task ID or prior attempt/error history. This is the preferred way to reroute future execution to a different adapter/provider/model. The CLI rejects `RUNNING`, `SUCCEEDED`, and `CANCELLED` task updates so active execution semantics and terminal audit history are not rewritten.
66
+
65
67
  For a ChatGPT worker, use the chatgpt adapter with capabilities model,chatgpt,browser-harness. The runtime launches the installed `chatgpt-browser-worker` agent, stores its observed `thread_id` as the durable task threadId, and reuses that ID on retry. Pass --chatgpt-project when the worker must run inside one exact ChatGPT Project. Set `AGENTS_RELAY_CHATGPT_BROWSER_WORKER_AGENT` to an explicit agent definition path when needed; otherwise standard skill roots are searched. Never place credentials or private prompts in job/task markers.
66
68
 
67
69
  Managed Codex and ChatGPT worker prompts are enriched at launch with the PR URL and durable job/task/parent/project identity; do not duplicate that context manually in task input. Keep one ChatGPT conversation per logical task. Retries reuse the same task conversation, but sibling tasks use separate conversations even when their adapter is the same.