@agentworkforce/workload-router 0.5.0 → 0.5.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/CHANGELOG.md +18 -0
- package/README.md +9 -107
- package/dist/generated/personas.d.ts +263 -0
- package/dist/generated/personas.d.ts.map +1 -1
- package/dist/generated/personas.js +215 -0
- package/dist/generated/personas.js.map +1 -1
- package/dist/index.d.ts +14 -229
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -481
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +53 -193
- package/dist/index.test.js.map +1 -1
- package/package.json +1 -4
- package/routing-profiles/default.json +29 -88
- package/routing-profiles/schema.json +16 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { resolve as resolvePath } from 'node:path';
|
|
4
|
-
import { frontendImplementer, codeReviewer, architecturePlanner, requirementsAnalyst, debuggerPersona, securityReviewer, technicalWriter, verifierPersona, testStrategist, tddGuard, flakeHunter, opencodeWorkflowSpecialist, npmProvenancePublisher, cloudSandboxInfra, sageSlackEgressMigrator, sageProactiveRewirer, cloudSlackProxyGuard, agentRelayE2eConductor, capabilityDiscoverer, posthogAgent, personaMaker, antiSlopAuditor } from './generated/personas.js';
|
|
1
|
+
import { frontendImplementer, codeReviewer, architecturePlanner, requirementsAnalyst, debuggerPersona, securityReviewer, technicalWriter, verifierPersona, testStrategist, tddGuard, flakeHunter, opencodeWorkflowSpecialist, npmProvenancePublisher, cloudSandboxInfra, sageSlackEgressMigrator, sageProactiveRewirer, cloudSlackProxyGuard, agentRelayE2eConductor, capabilityDiscoverer, npmPackageBundlerGuard, posthogAgent, personaMaker, antiSlopAuditor, apiContractReviewer, dockerStackWrangler, e2eValidator, integrationTestAuthor, agentRelayWorkflow, relayOrchestrator } from './generated/personas.js';
|
|
5
2
|
import defaultRoutingProfileJson from '../routing-profiles/default.json' with { type: 'json' };
|
|
6
3
|
export const HARNESS_VALUES = ['opencode', 'codex', 'claude'];
|
|
7
4
|
export const PERSONA_TIERS = ['best', 'best-value', 'minimum'];
|
|
@@ -36,9 +33,16 @@ export const PERSONA_INTENTS = [
|
|
|
36
33
|
'cloud-slack-proxy-guard',
|
|
37
34
|
'sage-cloud-e2e-conduction',
|
|
38
35
|
'capability-discovery',
|
|
36
|
+
'npm-package-compat',
|
|
39
37
|
'posthog',
|
|
40
38
|
'persona-authoring',
|
|
41
|
-
'
|
|
39
|
+
'agent-relay-workflow',
|
|
40
|
+
'slop-audit',
|
|
41
|
+
'api-contract-review',
|
|
42
|
+
'local-stack-orchestration',
|
|
43
|
+
'e2e-validation',
|
|
44
|
+
'write-integration-tests',
|
|
45
|
+
'relay-orchestrator'
|
|
42
46
|
];
|
|
43
47
|
export const PERMISSION_MODES = [
|
|
44
48
|
'default',
|
|
@@ -68,24 +72,6 @@ export const HARNESS_SKILL_TARGETS = {
|
|
|
68
72
|
codex: { asFlag: 'codex', dir: '.agents/skills' },
|
|
69
73
|
opencode: { asFlag: 'opencode', dir: '.skills' }
|
|
70
74
|
};
|
|
71
|
-
export class PersonaExecutionError extends Error {
|
|
72
|
-
result;
|
|
73
|
-
cause;
|
|
74
|
-
constructor(message, result, cause) {
|
|
75
|
-
super(message, cause === undefined ? undefined : { cause });
|
|
76
|
-
this.name = 'PersonaExecutionError';
|
|
77
|
-
this.result = result;
|
|
78
|
-
this.cause = cause;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
class CapturedCommandError extends Error {
|
|
82
|
-
capture;
|
|
83
|
-
constructor(message, capture) {
|
|
84
|
-
super(message);
|
|
85
|
-
this.name = 'CapturedCommandError';
|
|
86
|
-
this.capture = capture;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
75
|
const PRPM_URL_RE = /^https?:\/\/prpm\.dev\/packages\/([^/\s?#]+)\/([^/\s?#]+)\/?(?:[?#].*)?$/i;
|
|
90
76
|
const PRPM_BARE_REF_RE = /^([^/\s]+)\/([^/\s]+)$/;
|
|
91
77
|
function lastSegment(ref) {
|
|
@@ -241,7 +227,7 @@ export function materializeSkills(skills, harness, options = {}) {
|
|
|
241
227
|
const installedDir = installRoot !== undefined ? `${installRoot}/${repoRelativeDir}` : repoRelativeDir;
|
|
242
228
|
// When the plan stages into `installRoot`, cleanup targets the whole
|
|
243
229
|
// session dir (handled at plan level in buildCleanupArtifacts). Leave
|
|
244
|
-
// per-skill cleanupPaths empty so
|
|
230
|
+
// per-skill cleanupPaths empty so callers running individual
|
|
245
231
|
// install.cleanupPaths don't accidentally remove unrelated things.
|
|
246
232
|
const cleanupPaths = installRoot !== undefined
|
|
247
233
|
? Object.freeze([])
|
|
@@ -381,55 +367,6 @@ function buildCleanupArtifacts(plan) {
|
|
|
381
367
|
cleanupCommandString
|
|
382
368
|
};
|
|
383
369
|
}
|
|
384
|
-
function buildExecutionTask(systemPrompt, task, inputs) {
|
|
385
|
-
const sections = [`System Instructions:\n${systemPrompt.trim()}`, `Task:\n${task.trim()}`];
|
|
386
|
-
if (inputs && Object.keys(inputs).length > 0) {
|
|
387
|
-
sections.push(`Additional Inputs (JSON):\n${JSON.stringify(inputs, null, 2)}`);
|
|
388
|
-
}
|
|
389
|
-
return sections.join('\n\n');
|
|
390
|
-
}
|
|
391
|
-
function hash8(value) {
|
|
392
|
-
return createHash('sha256').update(value).digest('hex').slice(0, 8);
|
|
393
|
-
}
|
|
394
|
-
function sanitizeExecutionName(value) {
|
|
395
|
-
const sanitized = value
|
|
396
|
-
.trim()
|
|
397
|
-
.toLowerCase()
|
|
398
|
-
.replace(/[^a-z0-9_.-]+/g, '-')
|
|
399
|
-
.replace(/^-+|-+$/g, '');
|
|
400
|
-
return sanitized || `persona-${hash8(value)}`;
|
|
401
|
-
}
|
|
402
|
-
function createDeferred() {
|
|
403
|
-
let settled = false;
|
|
404
|
-
let resolveFn;
|
|
405
|
-
let rejectFn;
|
|
406
|
-
const promise = new Promise((resolve, reject) => {
|
|
407
|
-
resolveFn = (value) => {
|
|
408
|
-
settled = true;
|
|
409
|
-
resolve(value);
|
|
410
|
-
};
|
|
411
|
-
rejectFn = (reason) => {
|
|
412
|
-
settled = true;
|
|
413
|
-
reject(reason);
|
|
414
|
-
};
|
|
415
|
-
});
|
|
416
|
-
return {
|
|
417
|
-
promise,
|
|
418
|
-
resolve: resolveFn,
|
|
419
|
-
reject: rejectFn,
|
|
420
|
-
get settled() {
|
|
421
|
-
return settled;
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
function createAbortError(message) {
|
|
426
|
-
const error = new Error(message);
|
|
427
|
-
error.name = 'AbortError';
|
|
428
|
-
return error;
|
|
429
|
-
}
|
|
430
|
-
function isTimeoutError(message) {
|
|
431
|
-
return typeof message === 'string' && /timed out/i.test(message);
|
|
432
|
-
}
|
|
433
370
|
function deepFreeze(value) {
|
|
434
371
|
if (value === null || value === undefined || typeof value !== 'object') {
|
|
435
372
|
return value;
|
|
@@ -445,177 +382,6 @@ function deepFreeze(value) {
|
|
|
445
382
|
}
|
|
446
383
|
return Object.freeze(value);
|
|
447
384
|
}
|
|
448
|
-
function linkAbortSignal(signal, controller) {
|
|
449
|
-
if (!signal) {
|
|
450
|
-
return () => { };
|
|
451
|
-
}
|
|
452
|
-
if (signal.aborted) {
|
|
453
|
-
controller.abort(signal.reason);
|
|
454
|
-
return () => { };
|
|
455
|
-
}
|
|
456
|
-
const onAbort = () => controller.abort(signal.reason);
|
|
457
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
458
|
-
return () => signal.removeEventListener('abort', onAbort);
|
|
459
|
-
}
|
|
460
|
-
async function runCapturedCommand(options) {
|
|
461
|
-
const { command, args, cwd, env, timeoutMs, signal, onSpawn, onProgress } = options;
|
|
462
|
-
return new Promise((resolve, reject) => {
|
|
463
|
-
if (signal?.aborted) {
|
|
464
|
-
reject(createAbortError('Execution aborted before the process started'));
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
const child = spawn(command, args, {
|
|
468
|
-
cwd,
|
|
469
|
-
env,
|
|
470
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
471
|
-
});
|
|
472
|
-
let stdout = '';
|
|
473
|
-
let stderr = '';
|
|
474
|
-
let timedOut = false;
|
|
475
|
-
let timeoutId;
|
|
476
|
-
let killId;
|
|
477
|
-
let abortDelayId;
|
|
478
|
-
const cleanup = () => {
|
|
479
|
-
if (timeoutId) {
|
|
480
|
-
clearTimeout(timeoutId);
|
|
481
|
-
}
|
|
482
|
-
if (killId) {
|
|
483
|
-
clearTimeout(killId);
|
|
484
|
-
}
|
|
485
|
-
if (abortDelayId) {
|
|
486
|
-
clearTimeout(abortDelayId);
|
|
487
|
-
}
|
|
488
|
-
if (signal && abortHandler) {
|
|
489
|
-
signal.removeEventListener('abort', abortHandler);
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
const terminate = () => {
|
|
493
|
-
child.kill('SIGTERM');
|
|
494
|
-
killId = setTimeout(() => child.kill('SIGKILL'), 5_000);
|
|
495
|
-
killId.unref?.();
|
|
496
|
-
};
|
|
497
|
-
const abortHandler = () => {
|
|
498
|
-
if (stdout.length === 0 && stderr.length === 0) {
|
|
499
|
-
abortDelayId = setTimeout(() => {
|
|
500
|
-
abortDelayId = undefined;
|
|
501
|
-
terminate();
|
|
502
|
-
}, 15);
|
|
503
|
-
abortDelayId.unref?.();
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
terminate();
|
|
507
|
-
};
|
|
508
|
-
if (signal) {
|
|
509
|
-
signal.addEventListener('abort', abortHandler, { once: true });
|
|
510
|
-
}
|
|
511
|
-
if (timeoutMs !== undefined) {
|
|
512
|
-
timeoutId = setTimeout(() => {
|
|
513
|
-
timedOut = true;
|
|
514
|
-
child.kill('SIGTERM');
|
|
515
|
-
killId = setTimeout(() => child.kill('SIGKILL'), 5_000);
|
|
516
|
-
killId.unref?.();
|
|
517
|
-
}, timeoutMs);
|
|
518
|
-
timeoutId.unref?.();
|
|
519
|
-
}
|
|
520
|
-
child.stdout?.on('data', (chunk) => {
|
|
521
|
-
const text = chunk.toString();
|
|
522
|
-
stdout += text;
|
|
523
|
-
onProgress?.({ stream: 'stdout', text });
|
|
524
|
-
});
|
|
525
|
-
child.stderr?.on('data', (chunk) => {
|
|
526
|
-
const text = chunk.toString();
|
|
527
|
-
stderr += text;
|
|
528
|
-
onProgress?.({ stream: 'stderr', text });
|
|
529
|
-
});
|
|
530
|
-
onSpawn?.();
|
|
531
|
-
child.once('error', (error) => {
|
|
532
|
-
cleanup();
|
|
533
|
-
reject(error);
|
|
534
|
-
});
|
|
535
|
-
child.once('close', (code, exitSignal) => {
|
|
536
|
-
cleanup();
|
|
537
|
-
const capture = {
|
|
538
|
-
stdout,
|
|
539
|
-
stderr,
|
|
540
|
-
exitCode: code,
|
|
541
|
-
exitSignal: exitSignal ?? null
|
|
542
|
-
};
|
|
543
|
-
if (signal?.aborted) {
|
|
544
|
-
const error = createAbortError('Execution cancelled');
|
|
545
|
-
Object.assign(error, { capture });
|
|
546
|
-
reject(error);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
if (timedOut) {
|
|
550
|
-
reject(new CapturedCommandError(`Command timed out after ${timeoutMs ?? 'unknown'}ms`, capture));
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
resolve(capture);
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
function createLocalExecutor(stepCaptures, options, buildCommand) {
|
|
558
|
-
const execute = async (stepName, command, args, cwd, timeoutMs, ignoreExitCode = false) => {
|
|
559
|
-
const partialCapture = {
|
|
560
|
-
stdout: '',
|
|
561
|
-
stderr: '',
|
|
562
|
-
exitCode: null,
|
|
563
|
-
exitSignal: null
|
|
564
|
-
};
|
|
565
|
-
try {
|
|
566
|
-
const capture = await runCapturedCommand({
|
|
567
|
-
command,
|
|
568
|
-
args,
|
|
569
|
-
cwd,
|
|
570
|
-
env: options.env,
|
|
571
|
-
timeoutMs,
|
|
572
|
-
signal: options.signal,
|
|
573
|
-
onSpawn: () => {
|
|
574
|
-
stepCaptures.set(stepName, { ...partialCapture });
|
|
575
|
-
options.onStepSpawn?.(stepName);
|
|
576
|
-
},
|
|
577
|
-
onProgress: (chunk) => {
|
|
578
|
-
if (chunk.stream === 'stdout') {
|
|
579
|
-
partialCapture.stdout += chunk.text;
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
partialCapture.stderr += chunk.text;
|
|
583
|
-
}
|
|
584
|
-
stepCaptures.set(stepName, { ...partialCapture });
|
|
585
|
-
options.onStepProgress?.(stepName, chunk);
|
|
586
|
-
options.onProgress?.(chunk);
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
stepCaptures.set(stepName, capture);
|
|
590
|
-
if (!ignoreExitCode && capture.exitCode !== null && capture.exitCode !== 0) {
|
|
591
|
-
throw new CapturedCommandError(`Step "${stepName}" exited with code ${capture.exitCode}`, capture);
|
|
592
|
-
}
|
|
593
|
-
return capture;
|
|
594
|
-
}
|
|
595
|
-
catch (error) {
|
|
596
|
-
const capture = error instanceof CapturedCommandError ? error.capture : error.capture;
|
|
597
|
-
if (capture) {
|
|
598
|
-
stepCaptures.set(stepName, capture);
|
|
599
|
-
}
|
|
600
|
-
throw error;
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
return {
|
|
604
|
-
async executeAgentStep(step, agentDef, resolvedTask, timeoutMs) {
|
|
605
|
-
const extraArgs = agentDef.constraints?.model ? ['--model', agentDef.constraints.model] : undefined;
|
|
606
|
-
const [command, ...args] = buildCommand(agentDef.cli, extraArgs, resolvedTask);
|
|
607
|
-
const capture = await execute(step.name, command, args, resolvePath(step.cwd ?? options.cwd), timeoutMs, agentDef.cli === 'opencode');
|
|
608
|
-
return capture.stdout;
|
|
609
|
-
},
|
|
610
|
-
async executeDeterministicStep(step, resolvedCommand, cwd) {
|
|
611
|
-
const capture = await execute(step.name, 'sh', ['-c', resolvedCommand], resolvePath(cwd), step.timeoutMs);
|
|
612
|
-
return {
|
|
613
|
-
output: capture.stdout,
|
|
614
|
-
exitCode: capture.exitCode ?? 0
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
385
|
function isObject(value) {
|
|
620
386
|
return typeof value === 'object' && value !== null;
|
|
621
387
|
}
|
|
@@ -884,9 +650,16 @@ export const personaCatalog = {
|
|
|
884
650
|
'cloud-slack-proxy-guard': parsePersonaSpec(cloudSlackProxyGuard, 'cloud-slack-proxy-guard'),
|
|
885
651
|
'sage-cloud-e2e-conduction': parsePersonaSpec(agentRelayE2eConductor, 'sage-cloud-e2e-conduction'),
|
|
886
652
|
'capability-discovery': parsePersonaSpec(capabilityDiscoverer, 'capability-discovery'),
|
|
653
|
+
'npm-package-compat': parsePersonaSpec(npmPackageBundlerGuard, 'npm-package-compat'),
|
|
887
654
|
posthog: parsePersonaSpec(posthogAgent, 'posthog'),
|
|
888
655
|
'persona-authoring': parsePersonaSpec(personaMaker, 'persona-authoring'),
|
|
889
|
-
'
|
|
656
|
+
'agent-relay-workflow': parsePersonaSpec(agentRelayWorkflow, 'agent-relay-workflow'),
|
|
657
|
+
'slop-audit': parsePersonaSpec(antiSlopAuditor, 'slop-audit'),
|
|
658
|
+
'api-contract-review': parsePersonaSpec(apiContractReviewer, 'api-contract-review'),
|
|
659
|
+
'local-stack-orchestration': parsePersonaSpec(dockerStackWrangler, 'local-stack-orchestration'),
|
|
660
|
+
'e2e-validation': parsePersonaSpec(e2eValidator, 'e2e-validation'),
|
|
661
|
+
'write-integration-tests': parsePersonaSpec(integrationTestAuthor, 'write-integration-tests'),
|
|
662
|
+
'relay-orchestrator': parsePersonaSpec(relayOrchestrator, 'relay-orchestrator')
|
|
890
663
|
};
|
|
891
664
|
export const routingProfiles = {
|
|
892
665
|
default: parseRoutingProfile(defaultRoutingProfileJson, 'routingProfiles.default')
|
|
@@ -925,68 +698,19 @@ export function resolvePersonaByTier(intent, tier = 'best-value') {
|
|
|
925
698
|
}
|
|
926
699
|
/**
|
|
927
700
|
* Resolve a persona for `intent` and return a {@link PersonaContext}
|
|
928
|
-
* bundling the resolved persona
|
|
929
|
-
* `sendMessage()` closure for running the persona against a task.
|
|
701
|
+
* bundling the resolved persona and grouped install metadata.
|
|
930
702
|
*
|
|
931
703
|
* **This is not a React hook.** The `use*` prefix is unfortunate — it is
|
|
932
704
|
* a plain synchronous factory with no implicit state, no side effects,
|
|
933
705
|
* and no rules-of-hooks constraints. Calling `usePersona(intent)` does
|
|
934
706
|
* nothing but resolve routing config and pre-compute the install plan.
|
|
935
|
-
* Nothing is installed, spawned, or written to disk until you
|
|
936
|
-
* `
|
|
937
|
-
*
|
|
938
|
-
* See {@link PersonaContext} for the two usage modes (let `sendMessage()`
|
|
939
|
-
* handle install vs. pre-stage install and pass `installSkills: false`)
|
|
940
|
-
* and the double-install caveat.
|
|
707
|
+
* Nothing is installed, spawned, or written to disk until you run
|
|
708
|
+
* `install.commandString` yourself.
|
|
941
709
|
*
|
|
942
710
|
* @example
|
|
943
|
-
*
|
|
944
|
-
* // Only `status: 'completed'` resolves; non-zero exits / timeouts throw
|
|
945
|
-
* // PersonaExecutionError and cancellation throws AbortError, both with
|
|
946
|
-
* // the typed ExecuteResult attached as `err.result`.
|
|
947
|
-
* const { sendMessage } = usePersona('npm-provenance');
|
|
948
|
-
* try {
|
|
949
|
-
* const result = await sendMessage('Set up npm trusted publishing for this repo', {
|
|
950
|
-
* workingDirectory: '.',
|
|
951
|
-
* timeoutSeconds: 600,
|
|
952
|
-
* });
|
|
953
|
-
* // result.status === 'completed' here
|
|
954
|
-
* } catch (err) {
|
|
955
|
-
* const execErr = err as Error & { result?: ExecuteResult };
|
|
956
|
-
* console.error('persona run failed', execErr.result?.status, execErr.result?.stderr);
|
|
957
|
-
* }
|
|
958
|
-
*
|
|
959
|
-
* @example
|
|
960
|
-
* // Mode B — pre-stage install out-of-band (e.g. in a Dockerfile), then
|
|
961
|
-
* // run at runtime without re-installing:
|
|
962
|
-
* const { install, sendMessage } = usePersona('npm-provenance');
|
|
963
|
-
* // build/CI step:
|
|
711
|
+
* const { selection, install } = usePersona('npm-provenance');
|
|
964
712
|
* spawnSync(install.commandString, { shell: true, stdio: 'inherit' });
|
|
965
|
-
* //
|
|
966
|
-
* const result = await sendMessage('Your task', {
|
|
967
|
-
* workingDirectory: '.',
|
|
968
|
-
* installSkills: false,
|
|
969
|
-
* });
|
|
970
|
-
*
|
|
971
|
-
* @example
|
|
972
|
-
* // Cancellation + streaming progress. Aborting causes `await run` to
|
|
973
|
-
* // throw an AbortError with `err.result.status === 'cancelled'`, so
|
|
974
|
-
* // wrap in try/catch if you plan to abort.
|
|
975
|
-
* const abort = new AbortController();
|
|
976
|
-
* const run = usePersona('npm-provenance').sendMessage('Your task', {
|
|
977
|
-
* signal: abort.signal,
|
|
978
|
-
* onProgress: ({ stream, text }) => process[stream].write(text),
|
|
979
|
-
* });
|
|
980
|
-
* run.runId.then((id) => console.log('workflow run id:', id));
|
|
981
|
-
* // ...later:
|
|
982
|
-
* abort.abort(); // or: run.cancel('user requested');
|
|
983
|
-
* try {
|
|
984
|
-
* const result = await run;
|
|
985
|
-
* // result.status === 'completed'
|
|
986
|
-
* } catch (err) {
|
|
987
|
-
* const execErr = err as Error & { result?: ExecuteResult };
|
|
988
|
-
* // execErr.name === 'AbortError' and execErr.result?.status === 'cancelled'
|
|
989
|
-
* }
|
|
713
|
+
* // hand `selection` to your harness launcher of choice.
|
|
990
714
|
*
|
|
991
715
|
* @param intent The persona intent to resolve (e.g. `'npm-provenance'`).
|
|
992
716
|
* @param options Optional overrides. `harness` forces a specific harness
|
|
@@ -1008,7 +732,7 @@ export function usePersona(intent, options = {}) {
|
|
|
1008
732
|
* Same as {@link usePersona}, but takes a pre-resolved {@link PersonaSelection}
|
|
1009
733
|
* instead of an intent. Use this when you have a selection produced outside
|
|
1010
734
|
* the standard repo catalog — for example, a user-local persona override
|
|
1011
|
-
* loaded from disk
|
|
735
|
+
* loaded from disk.
|
|
1012
736
|
*/
|
|
1013
737
|
export function useSelection(baseSelection, options = {}) {
|
|
1014
738
|
const effectiveHarness = options.harness ?? baseSelection.runtime.harness;
|
|
@@ -1036,189 +760,9 @@ export function useSelection(baseSelection, options = {}) {
|
|
|
1036
760
|
cleanupCommand,
|
|
1037
761
|
cleanupCommandString
|
|
1038
762
|
});
|
|
1039
|
-
const sendMessage = (task, sendMessageOptions = {}) => {
|
|
1040
|
-
const runId = createDeferred();
|
|
1041
|
-
// The primary rejection path for any failure in sendMessage() is `resultPromise`
|
|
1042
|
-
// (which the caller awaits via `await execution`). `runId.promise` is an
|
|
1043
|
-
// auxiliary promise that mirrors the same rejection when early setup fails
|
|
1044
|
-
// before the workflow has actually started. Callers are not required to
|
|
1045
|
-
// consume `execution.runId`, so attach a no-op catch here to suppress
|
|
1046
|
-
// the unhandled-rejection warning (and, under Node's default
|
|
1047
|
-
// --unhandled-rejections=throw, an uncaught-exception crash) that would
|
|
1048
|
-
// otherwise fire when both of those conditions hold simultaneously.
|
|
1049
|
-
runId.promise.catch(() => { });
|
|
1050
|
-
const abortController = new AbortController();
|
|
1051
|
-
const unlinkAbort = linkAbortSignal(sendMessageOptions.signal, abortController);
|
|
1052
|
-
const stepName = sanitizeExecutionName(sendMessageOptions.name ?? `${frozenSelection.personaId}-${hash8(task)}`);
|
|
1053
|
-
const workflowName = `use-persona-${stepName}`;
|
|
1054
|
-
const installStepName = `${stepName}-install-skills`;
|
|
1055
|
-
const cleanupStepName = `${stepName}-cleanup-skills`;
|
|
1056
|
-
const workingDirectory = resolvePath(sendMessageOptions.workingDirectory ?? process.cwd());
|
|
1057
|
-
const timeoutMs = Math.max(1, Math.round((sendMessageOptions.timeoutSeconds ??
|
|
1058
|
-
frozenSelection.runtime.harnessSettings.timeoutSeconds) * 1000));
|
|
1059
|
-
const shouldInstallSkills = sendMessageOptions.installSkills !== false && frozenInstallPlan.installs.length > 0;
|
|
1060
|
-
const stepCaptures = new Map();
|
|
1061
|
-
let cancelReason;
|
|
1062
|
-
let workflowRunId;
|
|
1063
|
-
let runIdReadyTimer;
|
|
1064
|
-
const resolveRunId = (value = workflowRunId) => {
|
|
1065
|
-
if (runIdReadyTimer) {
|
|
1066
|
-
clearTimeout(runIdReadyTimer);
|
|
1067
|
-
runIdReadyTimer = undefined;
|
|
1068
|
-
}
|
|
1069
|
-
if (value && !runId.settled) {
|
|
1070
|
-
runId.resolve(value);
|
|
1071
|
-
}
|
|
1072
|
-
};
|
|
1073
|
-
const resultPromise = (async () => {
|
|
1074
|
-
try {
|
|
1075
|
-
const { InMemoryWorkflowDb, WorkflowRunner, buildCommand, workflow } = await import('@agent-relay/sdk/workflows');
|
|
1076
|
-
const executor = createLocalExecutor(stepCaptures, {
|
|
1077
|
-
cwd: workingDirectory,
|
|
1078
|
-
env: { ...process.env, ...sendMessageOptions.env },
|
|
1079
|
-
signal: abortController.signal,
|
|
1080
|
-
onStepSpawn: (startedStepName) => {
|
|
1081
|
-
if (startedStepName !== stepName || runId.settled || runIdReadyTimer) {
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
runIdReadyTimer = setTimeout(() => resolveRunId(), 250);
|
|
1085
|
-
runIdReadyTimer.unref?.();
|
|
1086
|
-
},
|
|
1087
|
-
onStepProgress: (progressStepName) => {
|
|
1088
|
-
if (progressStepName === stepName) {
|
|
1089
|
-
resolveRunId();
|
|
1090
|
-
}
|
|
1091
|
-
},
|
|
1092
|
-
onProgress: sendMessageOptions.onProgress
|
|
1093
|
-
}, buildCommand);
|
|
1094
|
-
const runner = new WorkflowRunner({
|
|
1095
|
-
cwd: workingDirectory,
|
|
1096
|
-
db: new InMemoryWorkflowDb(),
|
|
1097
|
-
executor
|
|
1098
|
-
});
|
|
1099
|
-
runner.on((event) => {
|
|
1100
|
-
if (event.type === 'run:started') {
|
|
1101
|
-
workflowRunId = event.runId;
|
|
1102
|
-
}
|
|
1103
|
-
if ((event.type === 'step:completed' || event.type === 'step:failed') &&
|
|
1104
|
-
event.stepName === stepName) {
|
|
1105
|
-
resolveRunId(event.runId);
|
|
1106
|
-
}
|
|
1107
|
-
});
|
|
1108
|
-
const agentName = `${stepName}-agent`;
|
|
1109
|
-
const builder = workflow(workflowName)
|
|
1110
|
-
.description(`Ad-hoc persona execution for ${frozenSelection.personaId}`)
|
|
1111
|
-
.pattern('dag')
|
|
1112
|
-
.timeout(timeoutMs)
|
|
1113
|
-
.trajectories(false)
|
|
1114
|
-
.agent(agentName, {
|
|
1115
|
-
cli: frozenSelection.runtime.harness,
|
|
1116
|
-
model: frozenSelection.runtime.model,
|
|
1117
|
-
role: frozenSelection.personaId,
|
|
1118
|
-
preset: 'worker',
|
|
1119
|
-
interactive: false,
|
|
1120
|
-
timeoutMs
|
|
1121
|
-
});
|
|
1122
|
-
if (shouldInstallSkills) {
|
|
1123
|
-
builder.step(installStepName, {
|
|
1124
|
-
type: 'deterministic',
|
|
1125
|
-
command: installCommandString,
|
|
1126
|
-
cwd: workingDirectory,
|
|
1127
|
-
timeoutMs,
|
|
1128
|
-
captureOutput: true,
|
|
1129
|
-
failOnError: true
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
builder.step(stepName, {
|
|
1133
|
-
agent: agentName,
|
|
1134
|
-
task: buildExecutionTask(frozenSelection.runtime.systemPrompt, task, sendMessageOptions.inputs),
|
|
1135
|
-
cwd: workingDirectory,
|
|
1136
|
-
timeoutMs,
|
|
1137
|
-
verification: { type: 'exit_code', value: '0' },
|
|
1138
|
-
...(shouldInstallSkills ? { dependsOn: [installStepName] } : {})
|
|
1139
|
-
});
|
|
1140
|
-
// Post-agent cleanup: removes the ephemeral skill artifact paths the
|
|
1141
|
-
// provider scattered during the install step. Only runs when this
|
|
1142
|
-
// sendMessage owns the install (Mode A) AND the agent step completed
|
|
1143
|
-
// — if the agent step fails or is skipped, the dag runner will skip
|
|
1144
|
-
// this step too, which is fine because (a) failure diagnostics stay
|
|
1145
|
-
// on disk for the user to inspect, and (b) `rm -rf` is idempotent so
|
|
1146
|
-
// a follow-up run can re-clean. The lockfile is deliberately not in
|
|
1147
|
-
// cleanupPaths, so repeat runs still benefit from cached resolution.
|
|
1148
|
-
if (shouldInstallSkills && frozenInstall.cleanupCommandString !== ':') {
|
|
1149
|
-
builder.step(cleanupStepName, {
|
|
1150
|
-
type: 'deterministic',
|
|
1151
|
-
command: frozenInstall.cleanupCommandString,
|
|
1152
|
-
cwd: workingDirectory,
|
|
1153
|
-
timeoutMs,
|
|
1154
|
-
captureOutput: true,
|
|
1155
|
-
failOnError: false,
|
|
1156
|
-
dependsOn: [stepName]
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
if (abortController.signal.aborted) {
|
|
1160
|
-
runner.abort();
|
|
1161
|
-
}
|
|
1162
|
-
else {
|
|
1163
|
-
abortController.signal.addEventListener('abort', () => runner.abort(), { once: true });
|
|
1164
|
-
}
|
|
1165
|
-
const run = (await runner.execute(builder.toConfig()));
|
|
1166
|
-
if (!runId.settled) {
|
|
1167
|
-
runId.resolve(run.id);
|
|
1168
|
-
}
|
|
1169
|
-
const primaryCapture = stepCaptures.get(stepName);
|
|
1170
|
-
const fallbackCapture = shouldInstallSkills ? stepCaptures.get(installStepName) : undefined;
|
|
1171
|
-
const capture = primaryCapture ?? fallbackCapture;
|
|
1172
|
-
const result = {
|
|
1173
|
-
status: run.status === 'cancelled'
|
|
1174
|
-
? 'cancelled'
|
|
1175
|
-
: run.status === 'failed' && isTimeoutError(run.error)
|
|
1176
|
-
? 'timeout'
|
|
1177
|
-
: run.status === 'completed'
|
|
1178
|
-
? 'completed'
|
|
1179
|
-
: 'failed',
|
|
1180
|
-
output: capture?.stdout ?? '',
|
|
1181
|
-
stderr: capture?.stderr ?? '',
|
|
1182
|
-
exitCode: capture?.exitCode ?? null,
|
|
1183
|
-
durationMs: Date.now() - (Date.parse(run.startedAt) || Date.now()),
|
|
1184
|
-
workflowRunId: run.id,
|
|
1185
|
-
stepName
|
|
1186
|
-
};
|
|
1187
|
-
if (run.status === 'completed') {
|
|
1188
|
-
return result;
|
|
1189
|
-
}
|
|
1190
|
-
if (run.status === 'cancelled') {
|
|
1191
|
-
const error = createAbortError(cancelReason ?? 'Execution cancelled');
|
|
1192
|
-
Object.assign(error, { result });
|
|
1193
|
-
throw error;
|
|
1194
|
-
}
|
|
1195
|
-
throw new PersonaExecutionError(run.error ?? `Persona execution failed for step "${stepName}"`, result);
|
|
1196
|
-
}
|
|
1197
|
-
catch (error) {
|
|
1198
|
-
if (!runId.settled) {
|
|
1199
|
-
runId.reject(error);
|
|
1200
|
-
}
|
|
1201
|
-
throw error;
|
|
1202
|
-
}
|
|
1203
|
-
finally {
|
|
1204
|
-
if (runIdReadyTimer) {
|
|
1205
|
-
clearTimeout(runIdReadyTimer);
|
|
1206
|
-
}
|
|
1207
|
-
unlinkAbort();
|
|
1208
|
-
}
|
|
1209
|
-
})();
|
|
1210
|
-
return Object.assign(resultPromise, {
|
|
1211
|
-
cancel(reason) {
|
|
1212
|
-
cancelReason = reason;
|
|
1213
|
-
abortController.abort(reason);
|
|
1214
|
-
},
|
|
1215
|
-
runId: runId.promise
|
|
1216
|
-
});
|
|
1217
|
-
};
|
|
1218
763
|
return Object.freeze({
|
|
1219
764
|
selection: frozenSelection,
|
|
1220
|
-
install: frozenInstall
|
|
1221
|
-
sendMessage
|
|
765
|
+
install: frozenInstall
|
|
1222
766
|
});
|
|
1223
767
|
}
|
|
1224
768
|
export * from './eval.js';
|