agents-relay 1.0.156 → 1.0.157
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/dist/cli-help.js +2 -2
- package/dist/cli-support.js +2 -24
- package/dist/cli.js +5 -5
- package/dist/dashboard-html.js +2 -2
- package/dist/reconciler-runtime.js +5 -1
- package/dist/reconciler.js +3 -1
- package/dist/task-fields.js +68 -0
- package/package.json +1 -1
- package/skills/agents-relay/contracts/task-job-contract.md +6 -0
package/dist/cli-help.js
CHANGED
|
@@ -46,7 +46,7 @@ Example:
|
|
|
46
46
|
|
|
47
47
|
Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
|
|
48
48
|
With --head, reuse/create the PR for that head. With --pr, bind an existing unmanaged PR.
|
|
49
|
-
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --project-path RELATIVE_PATH, --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review, --task-
|
|
49
|
+
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --project-path RELATIVE_PATH, --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review, --task-fields JSON. --objective and --objective-file are mutually exclusive. For a new --head Job, one is required and must contain enough standalone context to complete the Job without the originating chat/thread. Objective Markdown is also the default PR body; --body is an explicit override.
|
|
50
50
|
Job creation never creates executable tasks. The orchestrator decides the initial task batch separately. Re-running create against an already healthy Job is idempotent and does not mutate its metadata.
|
|
51
51
|
|
|
52
52
|
Example:
|
|
@@ -54,7 +54,7 @@ Example:
|
|
|
54
54
|
'job update': `Partially update mutable metadata on a healthy existing Job.
|
|
55
55
|
|
|
56
56
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
57
|
-
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --project-path RELATIVE_PATH, --priority, --mode fixed|autonomous, --schedule, --task-
|
|
57
|
+
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --project-path RELATIVE_PATH, --priority, --mode fixed|autonomous, --schedule, --task-fields JSON, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options. Updating the objective also updates the PR body.
|
|
58
58
|
Unspecified fields are preserved. Marker/binding corruption is never repaired implicitly; update fails with a precise diagnostic instead.
|
|
59
59
|
|
|
60
60
|
Example:
|
package/dist/cli-support.js
CHANGED
|
@@ -3,6 +3,7 @@ import { InMemoryStore, GitHubStore } from './store.js';
|
|
|
3
3
|
import { githubAuthContext } from './github-auth.js';
|
|
4
4
|
import { NatsEventBus, eventFor } from './events.js';
|
|
5
5
|
import { parseJobSchedule } from './scheduler.js';
|
|
6
|
+
import { parseJobTaskFieldsJson } from './task-fields.js';
|
|
6
7
|
export const SERVICE_DEFAULTS = { port: 8787, watchdogMs: 300000, webhookWatchdogMs: 300000, dashboardRefreshMs: 300000 };
|
|
7
8
|
export class ServiceEventBus {
|
|
8
9
|
upstream;
|
|
@@ -97,30 +98,7 @@ export function chatGptTabClosePolicy(args) {
|
|
|
97
98
|
return value;
|
|
98
99
|
}
|
|
99
100
|
export function requestedExecutionMode(args) { const value = arg(args, '--mode', arg(args, '--execution-mode')); return value ? executionMode(args) : undefined; }
|
|
100
|
-
export function
|
|
101
|
-
const raw = arg(args, '--task-status-markers');
|
|
102
|
-
if (!raw)
|
|
103
|
-
return undefined;
|
|
104
|
-
let parsed;
|
|
105
|
-
try {
|
|
106
|
-
parsed = JSON.parse(raw);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
throw new Error('--task-status-markers must be a JSON object');
|
|
110
|
-
}
|
|
111
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
112
|
-
throw new Error('--task-status-markers must be a JSON object');
|
|
113
|
-
const allowed = new Set(['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED']);
|
|
114
|
-
const out = {};
|
|
115
|
-
for (const [state, marker] of Object.entries(parsed)) {
|
|
116
|
-
if (!allowed.has(state))
|
|
117
|
-
throw new Error('--task-status-markers contains invalid Task state ' + state);
|
|
118
|
-
if (typeof marker !== 'string' || marker.length === 0 || marker.length > 32)
|
|
119
|
-
throw new Error('--task-status-markers value for ' + state + ' must be a non-empty string up to 32 characters');
|
|
120
|
-
out[state] = marker;
|
|
121
|
-
}
|
|
122
|
-
return { taskStatusMarkers: out };
|
|
123
|
-
}
|
|
101
|
+
export function taskFieldsFromArgs(args) { return parseJobTaskFieldsJson(arg(args, '--task-fields')); }
|
|
124
102
|
export function continuation(args) { const kind = arg(args, '--continue-kind'); const target = arg(args, '--continue-target'); return kind && target ? { kind: kind, target, input: arg(args, '--continue-input') || undefined } : null; }
|
|
125
103
|
export function scheduleFromArgs(args) {
|
|
126
104
|
const cron = arg(args, '--schedule');
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ import { runBlockerCommand } from './cli-blocker.js';
|
|
|
25
25
|
import { helpText, usage } from './cli-help.js';
|
|
26
26
|
import { parseAgentGraph } from './agent-graph.js';
|
|
27
27
|
export { helpText } from './cli-help.js';
|
|
28
|
-
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs,
|
|
28
|
+
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, taskFieldsFromArgs, requestManagedReconciliation, } from './cli-support.js';
|
|
29
29
|
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
30
30
|
export async function ensureManagedGitHubJob(client, repositoryName, trustedAuthors, options) {
|
|
31
31
|
if (!options.id)
|
|
@@ -51,7 +51,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
51
51
|
description: pr.body || undefined,
|
|
52
52
|
priority: options.priority ?? 'P2',
|
|
53
53
|
executionMode: options.executionMode ?? 'fixed',
|
|
54
|
-
|
|
54
|
+
taskFields: options.taskFields,
|
|
55
55
|
autoMerge: options.autoMerge ?? true,
|
|
56
56
|
reviewEnabled: options.reviewEnabled ?? true,
|
|
57
57
|
prNumber: pr.number,
|
|
@@ -343,7 +343,7 @@ async function runJobCommand(action, args) {
|
|
|
343
343
|
priority: priority(args),
|
|
344
344
|
executionMode: requestedExecutionMode(args),
|
|
345
345
|
schedule: scheduleFromArgs(args),
|
|
346
|
-
|
|
346
|
+
taskFields: taskFieldsFromArgs(args),
|
|
347
347
|
autoMerge: !hasArg(args, '--no-auto-merge'),
|
|
348
348
|
reviewEnabled: !hasArg(args, '--no-review'),
|
|
349
349
|
continuation: continuation(args),
|
|
@@ -408,8 +408,8 @@ async function runJobCommand(action, args) {
|
|
|
408
408
|
job.executionMode = executionMode(args, job.executionMode ?? 'fixed');
|
|
409
409
|
if (hasArg(args, '--schedule'))
|
|
410
410
|
job.schedule = scheduleFromArgs(args);
|
|
411
|
-
if (hasArg(args, '--task-
|
|
412
|
-
job.
|
|
411
|
+
if (hasArg(args, '--task-fields'))
|
|
412
|
+
job.taskFields = taskFieldsFromArgs(args);
|
|
413
413
|
if (hasArg(args, '--auto-merge') && hasArg(args, '--no-auto-merge'))
|
|
414
414
|
throw new Error('choose only one of --auto-merge or --no-auto-merge');
|
|
415
415
|
if (hasArg(args, '--auto-merge'))
|
package/dist/dashboard-html.js
CHANGED
|
@@ -320,8 +320,8 @@ function syncFilters(){
|
|
|
320
320
|
}
|
|
321
321
|
function repoName(repository){if(typeof repository!=='string'||!repository)return 'unknown-repo';const parts=repository.split('/').filter(Boolean);return parts.at(-1)||repository;}
|
|
322
322
|
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(job.id===selectedJob));row.addEventListener('click',()=>selectJob(job.id));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(job.id===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job.id);});select.append(element('span','job-title',text(repoName(job.repository)+'/PR#'+value(job.prNumber)+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.reviewEnabled!==false&&job.githubState!=='MERGED')states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(job.githubState==='MERGED'&&job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const primary=element('div','job-primary');primary.append(states);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
323
|
-
function
|
|
324
|
-
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key);const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next)collapsedTasks.add(key);else collapsedTasks.delete(key);});li.append(line,ul);}else li.append(line);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',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null: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)));const
|
|
323
|
+
function taskFieldDisplays(job,task){const out=[];for(const [name,value] of Object.entries(task.fields||{})){const def=job?.taskFields?.[name];const display=def?.values?.[value];if(typeof display==='string'&&display)out.push({name,label:def.label||name,value,display});}return out;}
|
|
324
|
+
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key);const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next)collapsedTasks.add(key);else collapsedTasks.delete(key);});li.append(line,ul);}else li.append(line);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',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null: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)));for(const field of taskFieldDisplays(selected(),task)){const badge=element('span','task-field',text(field.display));badge.title=field.label+': '+field.value;row.append(badge);}line.append(row);return li;}
|
|
325
325
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const work=tasks.filter(task=>task.kind!=='planner');if(!work.length){taskTree.append(element('li','empty',text('No work tasks in durable state.')));return;}const known=new Set(work.map(task=>task.id));const children=new Map();const roots=[];for(const task of work){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));}
|
|
326
326
|
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;}
|
|
327
327
|
const TASK_STATES=['DRAFT','QUEUED','RUNNING','BLOCKED','SUCCEEDED','FAILED','CANCELLED'];
|
|
@@ -5,6 +5,7 @@ import { eventFor } from './events.js';
|
|
|
5
5
|
import { WorkflowEffectExecutor } from './workflow/effects.js';
|
|
6
6
|
import { workflowEffectFailureEvent } from './workflow/contracts.js';
|
|
7
7
|
import { ReconcileCoordinator } from './reconcile-coordinator.js';
|
|
8
|
+
import { acceptedReportedTaskFields, eventDataWithoutTaskFields } from './task-fields.js';
|
|
8
9
|
export function executionEventSource(jobId, taskId, executionId) { return `job/${jobId}/task/${taskId}/execution/${executionId}`; }
|
|
9
10
|
function eventSourceId(event) { return typeof event.source?.id === 'string' ? event.source.id : undefined; }
|
|
10
11
|
const terminalExecutionClaims = new Set();
|
|
@@ -338,8 +339,11 @@ export class ReconcilerRuntimeBase {
|
|
|
338
339
|
task.updatedAt = event.timestamp || new Date().toISOString();
|
|
339
340
|
this.terminalWaiters.get(task.id)?.resolve();
|
|
340
341
|
this.terminalWaiters.delete(task.id);
|
|
342
|
+
const reportedFields = acceptedReportedTaskFields(job, event.data);
|
|
343
|
+
if (reportedFields)
|
|
344
|
+
task.fields = { ...(task.fields ?? {}), ...reportedFields };
|
|
341
345
|
if (event.type === 'task.completed') {
|
|
342
|
-
task.result = { summary: event.message || `Task ${task.id} completed`, data: event.data };
|
|
346
|
+
task.result = { summary: event.message || `Task ${task.id} completed`, data: eventDataWithoutTaskFields(event.data) };
|
|
343
347
|
task.error = null;
|
|
344
348
|
task.failureReason = undefined;
|
|
345
349
|
}
|
package/dist/reconciler.js
CHANGED
|
@@ -17,6 +17,7 @@ import { pendingTroubleshooterTasks } from './troubleshooter.js';
|
|
|
17
17
|
import { prepareManagedExecutionWorktree } from './workspace.js';
|
|
18
18
|
import { createReviewChairExecutionPlan } from './review-chair.js';
|
|
19
19
|
import { samePlannerChildDefinition } from './planner-child.js';
|
|
20
|
+
import { taskFieldsWorkerPrompt } from './task-fields.js';
|
|
20
21
|
const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
21
22
|
export function compareTaskPriority(a, b, jobPriority = 'P2') { const ar = PRIORITY_RANK[a.priority ?? jobPriority]; const br = PRIORITY_RANK[b.priority ?? jobPriority]; return ar - br || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
|
|
22
23
|
export async function managedWorkerInput(job, task, context) {
|
|
@@ -37,6 +38,7 @@ export async function managedWorkerInput(job, task, context) {
|
|
|
37
38
|
const output = task.output.kind === 'file'
|
|
38
39
|
? `file\nPath: ${task.output.path}`
|
|
39
40
|
: `task/PR\nPR: ${prUrl}\nTask: ${task.id}`;
|
|
41
|
+
const taskFields = taskFieldsWorkerPrompt(job);
|
|
40
42
|
return `[Agents Relay managed task]
|
|
41
43
|
Job URL: ${prUrl}
|
|
42
44
|
Job ID: ${job.id}
|
|
@@ -51,7 +53,7 @@ ${context ? executionContextPrompt(context) + '\n\n' : ''}${selectedAgent ? sele
|
|
|
51
53
|
[Output contract]
|
|
52
54
|
${output}
|
|
53
55
|
|
|
54
|
-
[Execution event contract — mandatory]
|
|
56
|
+
${taskFields}[Execution event contract — mandatory]
|
|
55
57
|
Use the canonical Neo events-bus with exactly Job ${job.id} and Task ${task.id}.
|
|
56
58
|
Your exact event source identity is: ${sourceId}
|
|
57
59
|
Every lifecycle event for this execution MUST set source.id to that exact string. Do not derive, rename, or replace it.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const FIELD_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
2
|
+
export function parseJobTaskFields(value, option = '--task-fields') {
|
|
3
|
+
if (value === undefined || value === null)
|
|
4
|
+
return undefined;
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
6
|
+
throw new Error(`${option} must be a JSON object`);
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const [name, rawDefinition] of Object.entries(value)) {
|
|
9
|
+
if (!FIELD_NAME.test(name) || name.length > 64)
|
|
10
|
+
throw new Error(`${option} contains invalid field name ${name}`);
|
|
11
|
+
if (!rawDefinition || typeof rawDefinition !== 'object' || Array.isArray(rawDefinition))
|
|
12
|
+
throw new Error(`${option}.${name} must be an object`);
|
|
13
|
+
const definition = rawDefinition;
|
|
14
|
+
const label = definition.label;
|
|
15
|
+
if (label !== undefined && (typeof label !== 'string' || !label.trim() || label.length > 64))
|
|
16
|
+
throw new Error(`${option}.${name}.label must be a non-empty string up to 64 characters`);
|
|
17
|
+
const rawValues = definition.values;
|
|
18
|
+
if (!rawValues || typeof rawValues !== 'object' || Array.isArray(rawValues) || Object.keys(rawValues).length === 0)
|
|
19
|
+
throw new Error(`${option}.${name}.values must be a non-empty object`);
|
|
20
|
+
const values = {};
|
|
21
|
+
for (const [fieldValue, display] of Object.entries(rawValues)) {
|
|
22
|
+
if (!FIELD_NAME.test(fieldValue) || fieldValue.length > 64)
|
|
23
|
+
throw new Error(`${option}.${name}.values contains invalid value ${fieldValue}`);
|
|
24
|
+
if (typeof display !== 'string' || !display || display.length > 32)
|
|
25
|
+
throw new Error(`${option}.${name}.values.${fieldValue} must be a non-empty string up to 32 characters`);
|
|
26
|
+
values[fieldValue] = display;
|
|
27
|
+
}
|
|
28
|
+
out[name] = { ...(typeof label === 'string' ? { label } : {}), values };
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
export function parseJobTaskFieldsJson(raw) {
|
|
33
|
+
if (!raw)
|
|
34
|
+
return undefined;
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error('--task-fields must be valid JSON');
|
|
41
|
+
}
|
|
42
|
+
return parseJobTaskFields(parsed);
|
|
43
|
+
}
|
|
44
|
+
export function acceptedReportedTaskFields(job, data) {
|
|
45
|
+
const raw = data?.taskFields;
|
|
46
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !job.taskFields)
|
|
47
|
+
return undefined;
|
|
48
|
+
const accepted = {};
|
|
49
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
50
|
+
if (typeof value !== 'string')
|
|
51
|
+
continue;
|
|
52
|
+
const definition = job.taskFields[name];
|
|
53
|
+
if (definition?.values[value] !== undefined)
|
|
54
|
+
accepted[name] = value;
|
|
55
|
+
}
|
|
56
|
+
return Object.keys(accepted).length ? accepted : undefined;
|
|
57
|
+
}
|
|
58
|
+
export function eventDataWithoutTaskFields(data) {
|
|
59
|
+
if (!data || !Object.prototype.hasOwnProperty.call(data, 'taskFields'))
|
|
60
|
+
return data;
|
|
61
|
+
const { taskFields: _taskFields, ...rest } = data;
|
|
62
|
+
return Object.keys(rest).length ? rest : undefined;
|
|
63
|
+
}
|
|
64
|
+
export function taskFieldsWorkerPrompt(job) {
|
|
65
|
+
if (!job.taskFields || Object.keys(job.taskFields).length === 0)
|
|
66
|
+
return '';
|
|
67
|
+
return `[Job-defined Task fields]\nThe Job defines these Task result fields: ${JSON.stringify(job.taskFields)}\nWhen a field applies to this execution, report its exact allowed value in the terminal event under data.taskFields. Example: data: { taskFields: { e2e: \"pass\" } }. These fields are separate from Task lifecycle state.\n\n`;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -50,6 +50,12 @@ For feature/refactor work with no failure to diagnose, root cause is not require
|
|
|
50
50
|
|
|
51
51
|
Keep these concepts distinct: observation is not root cause; hypothesis is not root cause; requested approach is not implemented solution; implementation is not validation.
|
|
52
52
|
|
|
53
|
+
### Job-defined Task fields
|
|
54
|
+
|
|
55
|
+
Canonical `Task.state` is reserved for Relay lifecycle only. A Job may declare `taskFields`, a map of Job-specific result fields. Each field declares an optional label and an allowed `values` map whose keys are durable values and whose values are display strings. A Task stores reported values separately in `fields`.
|
|
56
|
+
|
|
57
|
+
A model-backed worker reports applicable Job-defined values in its canonical terminal event under `data.taskFields`, for example `{"taskFields":{"e2e":"pass"}}`. Relay validates reported names and values against the Job declaration, copies only valid values into the Task marker's `fields`, and does not reinterpret them as lifecycle state. The raw `data.taskFields` transport payload is not duplicated inside `result.data`. Jobs without `taskFields` are unchanged.
|
|
58
|
+
|
|
53
59
|
## Worker routing order
|
|
54
60
|
|
|
55
61
|
Every new model-backed executable task follows this order:
|