@hunterzhu/pulse-runtime 0.1.5 → 0.1.7
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/context/builder.d.ts +6 -1
- package/dist/context/builder.js +37 -4
- package/dist/core/inbox.d.ts +6 -0
- package/dist/core/inbox.js +31 -7
- package/dist/core/mutations.d.ts +5 -1
- package/dist/core/mutations.js +5 -1
- package/dist/core/types.d.ts +26 -1
- package/dist/core/types.js +1 -1
- package/dist/dsl/program.d.ts +4 -1
- package/dist/dsl/program.js +147 -24
- package/dist/dsl/session.d.ts +7 -0
- package/dist/dsl/session.js +37 -10
- package/dist/dsl/templates.d.ts +2 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/scheduler/human-arbitration.d.ts +59 -0
- package/dist/scheduler/human-arbitration.js +62 -0
- package/dist/scheduler/runtime.d.ts +51 -3
- package/dist/scheduler/runtime.js +445 -40
- package/dist/scheduler/worker.d.ts +2 -0
- package/dist/scheduler/worker.js +24 -4
- package/dist/storage/session.d.ts +2 -1
- package/dist/storage/session.js +14 -0
- package/dist/transitions/validate.js +14 -4
- package/package.json +1 -1
package/dist/dsl/program.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { globalContextRef, laneContextRef } from '../core/types.js';
|
|
3
3
|
import { createDraftProxy } from './context-proxy.js';
|
|
4
|
-
import { contentHash, stableSerialize } from '../context/builder.js';
|
|
4
|
+
import { assertDslInstructionSize, contentHash, stableSerialize } from '../context/builder.js';
|
|
5
5
|
function target(step, fallback) {
|
|
6
6
|
if (typeof step === 'string')
|
|
7
7
|
return { step };
|
|
@@ -29,9 +29,7 @@ function scalarProjection(value) {
|
|
|
29
29
|
return Object.fromEntries(Object.entries(value).filter(([, child]) => child === null || typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean').map(([key, child]) => [key, child]));
|
|
30
30
|
}
|
|
31
31
|
function boundedInstruction(value) {
|
|
32
|
-
|
|
33
|
-
throw Object.assign(new Error('Instruction exceeds the 2 KB DSL limit.'), { code: 'INSTRUCTION_TOO_LARGE', retryable: false });
|
|
34
|
-
return value;
|
|
32
|
+
return assertDslInstructionSize(value);
|
|
35
33
|
}
|
|
36
34
|
function programLLMInput(config, input) {
|
|
37
35
|
return { ...input, ...(config.system === undefined ? {} : { system: config.system }), ...(config.toolSet === undefined ? {} : { toolSetId: config.toolSet }) };
|
|
@@ -201,6 +199,16 @@ function collectResumeResultRefs(input, refs) {
|
|
|
201
199
|
refs.add(ref);
|
|
202
200
|
}
|
|
203
201
|
}
|
|
202
|
+
/** Return a wait resolution even when the scheduler wrapped it in a control error. */
|
|
203
|
+
function waitResolution(input) {
|
|
204
|
+
if (!input)
|
|
205
|
+
return undefined;
|
|
206
|
+
if (input.type === 'wait')
|
|
207
|
+
return input.resolution;
|
|
208
|
+
if (input.type === 'control_error')
|
|
209
|
+
return waitResolution(input.original);
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
204
212
|
function annotateAction(action, derivedFrom) {
|
|
205
213
|
if (!derivedFrom.length)
|
|
206
214
|
return action;
|
|
@@ -213,7 +221,7 @@ function annotateAction(action, derivedFrom) {
|
|
|
213
221
|
const resultReaders = new WeakMap();
|
|
214
222
|
function readResult(ctx, ref) { return resultReaders.get(ctx)?.(ref); }
|
|
215
223
|
function waitFailure(ctx) {
|
|
216
|
-
const resolution = ctx.resumeInput
|
|
224
|
+
const resolution = waitResolution(ctx.resumeInput);
|
|
217
225
|
if (!resolution)
|
|
218
226
|
return undefined;
|
|
219
227
|
let dependencyError;
|
|
@@ -222,6 +230,10 @@ function waitFailure(ctx) {
|
|
|
222
230
|
dependencyError = dependency.outcome.error;
|
|
223
231
|
break;
|
|
224
232
|
}
|
|
233
|
+
if (dependency.state !== 'pending' && dependency.outcome.status === 'cancelled') {
|
|
234
|
+
dependencyError = dependency.outcome.error ?? { code: 'EFFECT_CANCELLED', message: dependency.outcome.reason ?? 'A waited effect was cancelled.', retryable: false };
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
225
237
|
}
|
|
226
238
|
if (dependencyError)
|
|
227
239
|
return dependencyError;
|
|
@@ -288,7 +300,7 @@ function makeContext(context, initialState) {
|
|
|
288
300
|
delta = { target: 'global', baseVersion: agent?.latestGlobalVersion ?? 0, sourceLaneId: context.lane.id, ops: clone(ops), ...(value.privacy === undefined ? {} : { privacy: value.privacy }), proposal: value.proposal };
|
|
289
301
|
};
|
|
290
302
|
const ctx = {
|
|
291
|
-
lane: context.lane, goal: context.lane.goal, global, globalVersion, laneState: readonlyState, history, now: context.now, ...(context.lane.progressWatchdog === undefined ? {} : { watchdog: context.lane.progressWatchdog }), ...(context.resumeInput ? { resumeInput: context.resumeInput } : {}),
|
|
303
|
+
lane: context.lane, goal: context.lane.goal, global, globalVersion, laneState: readonlyState, history, now: context.now, ...(context.lane.progressWatchdog === undefined ? {} : { watchdog: context.lane.progressWatchdog }), ...(context.resumeInput ? { resumeInput: context.resumeInput } : {}), ...(context.humanInputs?.length ? { humanInputs: context.humanInputs } : {}),
|
|
292
304
|
results: { meta: resultMeta, summary: (ref) => { if (context.state.results.has(ref) && resultVisible(context, ref))
|
|
293
305
|
derivedRefs.add(ref); return resultMeta(ref)?.summary; } },
|
|
294
306
|
mergeProposals: [...context.state.mergeProposals.values()].filter((proposal) => proposal.agentId === context.lane.agentId).map((proposal) => { for (const ref of proposal.delta.derivedFrom ?? [])
|
|
@@ -371,16 +383,34 @@ export class StepBuilder {
|
|
|
371
383
|
}
|
|
372
384
|
addReActLoopStep(name, options) {
|
|
373
385
|
this.compactionBoundaries.add(name);
|
|
386
|
+
// The decode step handles ReAct compaction after consuming the current
|
|
387
|
+
// model result. This preserves the wait's result references while the
|
|
388
|
+
// summary is being generated.
|
|
374
389
|
const readTurns = (ctx) => { const sdk = sdkLocals(ctx.lane.resume.locals); const turn = sdk[`${name}Turns`]; return typeof turn === 'number' && Number.isInteger(turn) && turn >= 0 ? turn : 0; };
|
|
375
390
|
const inputKey = `${name}Inputs`;
|
|
376
391
|
const readInputs = (ctx) => { const value = sdkLocals(ctx.lane.resume.locals)[inputKey]; return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; };
|
|
377
|
-
const writeTurns = (ctx, turns, inputs = readInputs(ctx)) => { const locals = ctx.lane.resume.locals; const base = locals && typeof locals === 'object' && !Array.isArray(locals) ? locals : {}; return { ...base, $sdk: { ...sdkLocals(locals), [`${name}Turns`]: turns, [inputKey]: asJson(
|
|
378
|
-
const
|
|
379
|
-
const
|
|
392
|
+
const writeTurns = (ctx, turns, inputs = readInputs(ctx)) => { const { conversation: _conversation, ...persistedInputs } = inputs; const locals = ctx.lane.resume.locals; const base = locals && typeof locals === 'object' && !Array.isArray(locals) ? locals : {}; return { ...base, $sdk: { ...sdkLocals(locals), [`${name}Turns`]: turns, [inputKey]: asJson(persistedInputs) } }; };
|
|
393
|
+
const pendingResultKey = `${name}PendingResultRef`;
|
|
394
|
+
const pendingResultRef = (ctx) => { const value = sdkLocals(ctx.lane.resume.locals)[pendingResultKey]; return typeof value === 'string' ? value : undefined; };
|
|
395
|
+
const clearPendingResult = (ctx) => {
|
|
396
|
+
const locals = ordinaryLocals(ctx.lane.resume.locals);
|
|
397
|
+
const sdk = { ...sdkLocals(ctx.lane.resume.locals) };
|
|
398
|
+
delete sdk[pendingResultKey];
|
|
399
|
+
return { ...locals, $sdk: sdk };
|
|
400
|
+
};
|
|
401
|
+
const resultRefFromWait = (ctx) => {
|
|
402
|
+
const pending = pendingResultRef(ctx);
|
|
403
|
+
if (pending)
|
|
404
|
+
return pending;
|
|
405
|
+
const resolution = waitResolution(ctx.resumeInput);
|
|
406
|
+
const dependency = resolution === undefined ? undefined : Object.values(resolution.dependencies).find((candidate) => candidate.state === 'settled');
|
|
407
|
+
return dependency?.state === 'settled' ? dependency.outcome.resultRef : undefined;
|
|
408
|
+
};
|
|
409
|
+
const resultRefsFromWait = (ctx) => { const resolution = waitResolution(ctx.resumeInput); return resolution === undefined ? [] : Object.values(resolution.dependencies).flatMap((dependency) => dependency.state === 'settled' && dependency.outcome.resultRef ? [dependency.outcome.resultRef] : []); };
|
|
380
410
|
const instruction = (ctx) => boundedInstruction(typeof options.instruction === 'string' ? options.instruction : options.instruction({ goal: ctx.goal, state: scalarProjection(ctx.laneState) }));
|
|
381
|
-
const submitModel = (ctx, turn, inputs = {}) => { const resultRefs = [...new Set(inputs.results ?? [])]; const findingRefs = [...new Set(inputs.findings ?? [])]; const artifactRefs = [...new Set(inputs.artifacts ?? [])]; const dataRefs = [...resultRefs, ...findingRefs, ...artifactRefs.map((ref) => ({ kind: 'artifact', ref }))]; const requirements = { ...(options.requirements ?? {}), ...(options.toolAllow === undefined ? {} : { toolCalling: true }) }; return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-turn-${turn}`, kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: options.task ?? 'reason', instruction: instruction(ctx), inputs: { ...(resultRefs.length ? { results: resultRefs } : {}), ...(findingRefs.length ? { findings: findingRefs } : {}), ...(artifactRefs.length ? { artifacts: artifactRefs } : {}), ...(inputs.events?.length ? { events: [...new Set(inputs.events)] } : {}) }, turn, ...(inputs.toolDiscovery === undefined ? {} : { toolDiscovery: inputs.toolDiscovery }), ...(options.outputSchema === undefined ? {} : { outputSchema: zodJsonSchema(options.outputSchema) }), ...(Object.keys(requirements).length ? { requirements } : {}) }), ...(dataRefs.length ? { derivedFrom: dataRefs } : {}) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: { programId: this.config.id, programVersion: this.config.version, step: `${name}:decode`, locals: writeTurns(ctx, turn, inputs) }, locals: writeTurns(ctx, turn, inputs) }; };
|
|
411
|
+
const submitModel = (ctx, turn, inputs = {}) => { const resultRefs = [...new Set(inputs.results ?? [])]; const findingRefs = [...new Set(inputs.findings ?? [])]; const artifactRefs = [...new Set(inputs.artifacts ?? [])]; const dataRefs = [...resultRefs, ...findingRefs, ...artifactRefs.map((ref) => ({ kind: 'artifact', ref }))]; const requirements = { ...(options.requirements ?? {}), ...(options.toolAllow === undefined ? {} : { toolCalling: true }) }; return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-turn-${turn}`, kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: options.task ?? 'reason', instruction: instruction(ctx), inputs: { ...(resultRefs.length ? { results: resultRefs } : {}), ...(findingRefs.length ? { findings: findingRefs } : {}), ...(artifactRefs.length ? { artifacts: artifactRefs } : {}), ...(inputs.events?.length ? { events: [...new Set(inputs.events)] } : {}), ...(inputs.conversation?.length ? { conversation: inputs.conversation } : {}) }, turn, ...(inputs.toolDiscovery === undefined ? {} : { toolDiscovery: inputs.toolDiscovery }), ...(options.outputSchema === undefined ? {} : { outputSchema: zodJsonSchema(options.outputSchema) }), ...(Object.keys(requirements).length ? { requirements } : {}) }), ...(dataRefs.length ? { derivedFrom: dataRefs } : {}) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: { programId: this.config.id, programVersion: this.config.version, step: `${name}:decode`, locals: writeTurns(ctx, turn, inputs) }, locals: writeTurns(ctx, turn, inputs) }; };
|
|
382
412
|
this.handlers.set(name, (ctx) => { const turn = readTurns(ctx) + 1; const inputs = options.inputs?.(ctx) ?? {}; const output = submitModel(ctx, turn, inputs); return { ...output, next: `${name}:decode` }; });
|
|
383
|
-
this.handlers.set(`${name}:tools`, (ctx) => { const turn = readTurns(ctx); const previous = readInputs(ctx); const inputs = { ...previous, results: [...new Set([...(previous.results ?? []), ...resultRefsFromWait(ctx)])] }; return { ...submitModel(ctx, turn + 1, inputs), next: `${name}:decode` }; });
|
|
413
|
+
this.handlers.set(`${name}:tools`, (ctx) => { const turn = readTurns(ctx); const previous = readInputs(ctx); const current = options.inputs?.(ctx) ?? {}; const inputs = { ...current, ...previous, results: [...new Set([...(previous.results ?? []), ...resultRefsFromWait(ctx)])] }; return { ...submitModel(ctx, turn + 1, inputs), next: `${name}:decode` }; });
|
|
384
414
|
this.handlers.set(`${name}:decode`, (ctx) => {
|
|
385
415
|
const turns = readTurns(ctx);
|
|
386
416
|
const ref = resultRefFromWait(ctx);
|
|
@@ -388,19 +418,33 @@ export class StepBuilder {
|
|
|
388
418
|
const record = value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
389
419
|
const finishReason = record?.finishReason;
|
|
390
420
|
const toolCalls = Array.isArray(record?.toolCalls) ? record.toolCalls : [];
|
|
391
|
-
const fail = (runtimeError) => options.onError ? { next: options.onError(runtimeError, ctx) } : (() => { const error = Object.assign(new Error(runtimeError.message), runtimeError); throw error; })();
|
|
421
|
+
const fail = (runtimeError) => options.onError ? { next: options.onError(runtimeError, ctx), locals: clearPendingResult(ctx) } : (() => { const error = Object.assign(new Error(runtimeError.message), runtimeError); throw error; })();
|
|
392
422
|
const dependencyError = waitFailure(ctx);
|
|
393
423
|
if (dependencyError)
|
|
394
424
|
return fail(dependencyError);
|
|
395
425
|
const maxTurns = Math.max(1, Math.floor(options.maxTurns ?? 10));
|
|
396
|
-
const maxTurnsReached = () => options.onMaxTurns ? { next: options.onMaxTurns(ctx) } : fail({ code: 'MAX_TURNS_REACHED', message: `ReAct loop ${name} reached its maximum of ${maxTurns} turns.`, retryable: false });
|
|
426
|
+
const maxTurnsReached = () => options.onMaxTurns ? { next: options.onMaxTurns(ctx), locals: clearPendingResult(ctx) } : fail({ code: 'MAX_TURNS_REACHED', message: `ReAct loop ${name} reached its maximum of ${maxTurns} turns.`, retryable: false });
|
|
427
|
+
// The current model result must be retained while older history is
|
|
428
|
+
// summarized. The generic compaction macro then returns to decode and
|
|
429
|
+
// this pending reference lets us continue processing the same result.
|
|
430
|
+
const compaction = this.config.historyCompaction;
|
|
431
|
+
const pressure = ctx.lane.historyPressure;
|
|
432
|
+
const shouldCompactAfterResult = compaction !== undefined && ref !== undefined && pendingResultRef(ctx) === undefined && sdkLocals(ctx.lane.resume.locals).compactPending !== true && pressure !== undefined && pressure.historyTokens > pressure.softTokens && ctx.lane.context.history.length > Math.max(0, Math.floor(compaction.keepRecentRounds));
|
|
433
|
+
if (shouldCompactAfterResult) {
|
|
434
|
+
const locals = ordinaryLocals(ctx.lane.resume.locals);
|
|
435
|
+
const sdk = sdkLocals(ctx.lane.resume.locals);
|
|
436
|
+
return { actions: [], next: '$compact:summarize', locals: { ...locals, $sdk: { ...sdk, compactPending: true, compactReturnStep: `${name}:decode`, [pendingResultKey]: ref } } };
|
|
437
|
+
}
|
|
397
438
|
if (finishReason === 'tool_calls') {
|
|
398
439
|
if (turns >= maxTurns || toolCalls.length === 0)
|
|
399
440
|
return maxTurnsReached();
|
|
400
441
|
const invalidTool = toolCalls.find((call) => { const item = call && typeof call === 'object' && !Array.isArray(call) ? call : {}; const toolName = typeof item.name === 'string' ? item.name : ''; return !toolName || (options.toolAllow !== undefined && !options.toolAllow.includes(toolName)); });
|
|
401
442
|
if (invalidTool !== undefined)
|
|
402
443
|
return fail({ code: 'ACTION_TOOL_NOT_ALLOWED', message: 'Model requested a tool outside the ReAct allow-list.', retryable: false });
|
|
403
|
-
const
|
|
444
|
+
const resolution = waitResolution(ctx.resumeInput);
|
|
445
|
+
const sourceEffectId = ref !== undefined && ctx.results.meta(ref)?.producer.kind === 'effect'
|
|
446
|
+
? ctx.results.meta(ref)?.producer.id
|
|
447
|
+
: resolution === undefined ? undefined : Object.values(resolution.dependencies).find((dependency) => dependency.state === 'settled' && dependency.target.kind === 'effect')?.target.id;
|
|
404
448
|
const sourcePrivacy = ref === undefined ? undefined : ctx.results.meta(ref)?.privacy;
|
|
405
449
|
const toolDerivedFrom = ref === undefined ? [] : [ref];
|
|
406
450
|
const calls = toolCalls.map((call, index) => {
|
|
@@ -409,6 +453,72 @@ export class StepBuilder {
|
|
|
409
453
|
const toolName = typeof item.name === 'string' ? item.name : '';
|
|
410
454
|
return { originalId, toolName, toolCallId: `${name}:${turns}:${originalId}`, input: item.input ?? {} };
|
|
411
455
|
});
|
|
456
|
+
const askCalls = calls.filter((call) => String(call.toolName).startsWith('ask.'));
|
|
457
|
+
if (askCalls.length > 0) {
|
|
458
|
+
if (askCalls.length !== calls.length)
|
|
459
|
+
return fail({ code: 'ASK_MIXED_TOOL_CALLS', message: 'An ask interaction must be requested in a separate model turn from workspace tools.', retryable: false });
|
|
460
|
+
if (askCalls.length !== 1)
|
|
461
|
+
return fail({ code: 'ASK_MULTIPLE_REQUESTS', message: 'Only one ask interaction may be requested at a time.', retryable: false });
|
|
462
|
+
const call = askCalls[0];
|
|
463
|
+
const rawInput = call.input && typeof call.input === 'object' && !Array.isArray(call.input) ? call.input : {};
|
|
464
|
+
const askType = call.toolName === 'ask.choice' ? 'choice' : call.toolName === 'ask.multi' ? 'multi' : call.toolName === 'ask.input' ? 'input' : undefined;
|
|
465
|
+
if (!askType)
|
|
466
|
+
return fail({ code: 'ASK_TOOL_UNKNOWN', message: `Unknown ask tool ${String(call.toolName)}.`, retryable: false });
|
|
467
|
+
if (typeof rawInput.prompt === 'string' && rawInput.prompt.length > 2_000)
|
|
468
|
+
return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask prompts must be 2000 characters or fewer.', retryable: false });
|
|
469
|
+
const prompt = typeof rawInput.prompt === 'string' && rawInput.prompt.trim() ? rawInput.prompt : `Pulse needs your input for ${askType}.`;
|
|
470
|
+
const askInput = { kind: 'ask', type: askType, toolName: String(call.toolName), toolCallId: String(call.toolCallId), prompt };
|
|
471
|
+
if (askType === 'choice' || askType === 'multi') {
|
|
472
|
+
const rawOptions = Array.isArray(rawInput.options) ? rawInput.options : [];
|
|
473
|
+
if (rawOptions.length > 50)
|
|
474
|
+
return fail({ code: 'ASK_OPTIONS_TOO_MANY', message: 'ask.choice and ask.multi accept at most 50 options.', retryable: false });
|
|
475
|
+
const seen = new Set();
|
|
476
|
+
const options = rawOptions.flatMap((option) => {
|
|
477
|
+
const item = typeof option === 'string' ? { label: option, value: option } : option && typeof option === 'object' && !Array.isArray(option) ? option : undefined;
|
|
478
|
+
if (!item || typeof item.label !== 'string' || typeof item.value !== 'string')
|
|
479
|
+
return [];
|
|
480
|
+
if (item.label.length === 0 || item.label.length > 500 || item.value.length === 0 || item.value.length > 500 || seen.has(item.value))
|
|
481
|
+
return [];
|
|
482
|
+
seen.add(item.value);
|
|
483
|
+
return [{ label: item.label, value: item.value }];
|
|
484
|
+
});
|
|
485
|
+
if (options.length === 0)
|
|
486
|
+
return fail({ code: 'ASK_OPTIONS_REQUIRED', message: 'ask.choice and ask.multi require at least one valid option.', retryable: false });
|
|
487
|
+
askInput.options = options;
|
|
488
|
+
if (askType === 'multi') {
|
|
489
|
+
const min = typeof rawInput.min === 'number' && Number.isInteger(rawInput.min) ? rawInput.min : undefined;
|
|
490
|
+
const max = typeof rawInput.max === 'number' && Number.isInteger(rawInput.max) ? rawInput.max : undefined;
|
|
491
|
+
if ((rawInput.min !== undefined && (min === undefined || min < 0)) || (rawInput.max !== undefined && (max === undefined || max < 1)) || (min !== undefined && max !== undefined && min > max))
|
|
492
|
+
return fail({ code: 'ASK_RANGE_INVALID', message: 'ask.multi min and max must be integers with min <= max.', retryable: false });
|
|
493
|
+
if (min !== undefined)
|
|
494
|
+
askInput.min = min;
|
|
495
|
+
if (max !== undefined)
|
|
496
|
+
askInput.max = max;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
if (typeof rawInput.placeholder === 'string') {
|
|
501
|
+
if (rawInput.placeholder.length > 500)
|
|
502
|
+
return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask placeholders must be 500 characters or fewer.', retryable: false });
|
|
503
|
+
askInput.placeholder = rawInput.placeholder;
|
|
504
|
+
}
|
|
505
|
+
if (typeof rawInput.defaultValue === 'string') {
|
|
506
|
+
if (rawInput.defaultValue.length > 2_000)
|
|
507
|
+
return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask default values must be 2000 characters or fewer.', retryable: false });
|
|
508
|
+
askInput.defaultValue = rawInput.defaultValue;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const humanEffect = {
|
|
512
|
+
key: `${name}-ask-${turns}`,
|
|
513
|
+
...(sourceEffectId === undefined ? {} : { llmEffectId: sourceEffectId }),
|
|
514
|
+
...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }),
|
|
515
|
+
...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}),
|
|
516
|
+
kind: 'human',
|
|
517
|
+
concurrencyClass: 'none',
|
|
518
|
+
input: askInput,
|
|
519
|
+
};
|
|
520
|
+
return { actions: [{ type: 'submit_effects', effects: [humanEffect], wait: { onUnsatisfied: 'resume_with_error' } }], next: `${name}:tools`, locals: clearPendingResult(ctx) };
|
|
521
|
+
}
|
|
412
522
|
const makeToolEffects = (approvedCalls) => ({ type: 'submit_effects', effects: approvedCalls.map((call, index) => ({ key: `${name}-tool-${turns}-${index + 1}`, toolCallId: String(call.toolCallId ?? `${name}:${turns}:${String(call.originalId)}`), ...(sourceEffectId === undefined ? {} : { llmEffectId: sourceEffectId }), ...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }), ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}), kind: 'tool', concurrencyClass: 'tool', input: { toolCallId: String(call.toolCallId ?? `${name}:${turns}:${String(call.originalId)}`), name: String(call.toolName), arguments: call.input, ...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }), ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}) } })), wait: { onUnsatisfied: 'resume_with_error' } });
|
|
413
523
|
if (options.toolApproval) {
|
|
414
524
|
const approvalKey = `${name}PendingToolCalls`;
|
|
@@ -419,13 +529,21 @@ export class StepBuilder {
|
|
|
419
529
|
const extra = typeof options.toolApproval.prompt === 'string' ? options.toolApproval.prompt : options.toolApproval.prompt(calls.map((call) => ({ name: call.toolName, toolCallId: call.originalId })), ctx);
|
|
420
530
|
const prompt = boundedInstruction(`Approve ${calls.length} tool call(s). Digest ${digest}.\n${listing}\n${extra}`);
|
|
421
531
|
this.handlers.set(`${name}:approval`, (approvalCtx) => {
|
|
422
|
-
const
|
|
532
|
+
const resolution = waitResolution(approvalCtx.resumeInput);
|
|
533
|
+
const dependency = resolution === undefined ? undefined : Object.values(resolution.dependencies).find((candidate) => candidate.state === 'settled');
|
|
423
534
|
const approvalError = waitFailure(approvalCtx);
|
|
424
535
|
if (approvalError)
|
|
425
536
|
return fail(approvalError);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
537
|
+
const settledDependency = dependency;
|
|
538
|
+
if (settledDependency?.state !== 'settled') {
|
|
539
|
+
return fail({
|
|
540
|
+
code: 'APPROVAL_RESPONSE_MISSING',
|
|
541
|
+
message: 'Approval response was not received.',
|
|
542
|
+
retryable: false,
|
|
543
|
+
details: { dependencyState: settledDependency?.state ?? 'missing', waitId: resolution?.waitId ?? null },
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
const value = settledDependency.outcome.resultRef ? readResult(approvalCtx, settledDependency.outcome.resultRef) : undefined;
|
|
429
547
|
const parsed = z.object({ approved: z.boolean(), reason: z.string().optional() }).safeParse(value);
|
|
430
548
|
if (!parsed.success)
|
|
431
549
|
return fail({ code: 'APPROVAL_RESPONSE_INVALID', message: 'Approval response must contain approved=true or false.', retryable: false, details: parsed.error.message });
|
|
@@ -447,7 +565,7 @@ export class StepBuilder {
|
|
|
447
565
|
});
|
|
448
566
|
return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-approval-${turns}`, kind: 'human', concurrencyClass: 'none', input: { prompt, digest, tools: calls }, ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: `${name}:approval`, locals: { ...locals, $sdk: { ...sdkLocals(locals), [approvalKey]: calls, [digestKey]: digest } } };
|
|
449
567
|
}
|
|
450
|
-
return { actions: [makeToolEffects(calls)], next: `${name}:tools
|
|
568
|
+
return { actions: [makeToolEffects(calls)], next: `${name}:tools`, locals: clearPendingResult(ctx) };
|
|
451
569
|
}
|
|
452
570
|
if (turns >= maxTurns)
|
|
453
571
|
return maxTurnsReached();
|
|
@@ -460,15 +578,15 @@ export class StepBuilder {
|
|
|
460
578
|
if (!ref)
|
|
461
579
|
return fail({ code: 'MISSING_RESULT_REF', message: 'ReAct result did not produce a ResultRef.', retryable: false });
|
|
462
580
|
if (typeof options.onFinish === 'function')
|
|
463
|
-
return { next: options.onFinish(ref, ctx) };
|
|
581
|
+
return { next: options.onFinish(ref, ctx), locals: clearPendingResult(ctx) };
|
|
464
582
|
if (options.onFinish.structured) {
|
|
465
583
|
const structuredValue = record?.structured ?? value;
|
|
466
584
|
const parsed = options.onFinish.structured.schema.safeParse(structuredValue);
|
|
467
585
|
if (!parsed.success)
|
|
468
586
|
return fail({ code: 'OUTPUT_SCHEMA_VIOLATION', message: 'ReAct structured result did not match schema.', retryable: false, details: parsed.error.message });
|
|
469
|
-
return { next: options.onFinish.structured.onParsed(parsed.data, ctx) };
|
|
587
|
+
return { next: options.onFinish.structured.onParsed(parsed.data, ctx), locals: clearPendingResult(ctx) };
|
|
470
588
|
}
|
|
471
|
-
return { next: options.onFinish.text(ref, ctx) };
|
|
589
|
+
return { next: options.onFinish.text(ref, ctx), locals: clearPendingResult(ctx) };
|
|
472
590
|
});
|
|
473
591
|
return this;
|
|
474
592
|
}
|
|
@@ -624,7 +742,7 @@ export class StepBuilder {
|
|
|
624
742
|
if (upToSeq === undefined)
|
|
625
743
|
return { next: returnStep === compactSummarize ? entry : returnStep, locals: { ...locals, $sdk: sdk } };
|
|
626
744
|
return {
|
|
627
|
-
actions: [{ type: 'submit_effects', effects: [{ key: '$compact-summary', kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: compaction.summarizeTask, historySeqs: candidates.map((record) => record.seq), upToSeq }) }], wait: { onUnsatisfied: 'resume_with_error' } }],
|
|
745
|
+
actions: [{ type: 'submit_effects', effects: [{ key: '$compact-summary', kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: compaction.summarizeTask, ...(compaction.instruction === undefined ? {} : { instruction: compaction.instruction }), historySeqs: candidates.map((record) => record.seq), upToSeq }) }], wait: { onUnsatisfied: 'resume_with_error' } }],
|
|
628
746
|
next: compactApply,
|
|
629
747
|
locals: { ...locals, $sdk: { ...sdk, compactPending: true, compactReturnStep: returnStep, compactUpToSeq: upToSeq } },
|
|
630
748
|
};
|
|
@@ -662,7 +780,12 @@ export class StepBuilder {
|
|
|
662
780
|
step: (context) => {
|
|
663
781
|
const sdk = sdkLocals(context.lane.resume.locals);
|
|
664
782
|
const pressure = context.lane.historyPressure;
|
|
665
|
-
const
|
|
783
|
+
const keepRecentRounds = compaction === undefined ? 0 : Math.max(0, Math.floor(compaction.keepRecentRounds));
|
|
784
|
+
const foldable = context.lane.context.history.slice(0, Math.max(0, context.lane.context.history.length - keepRecentRounds));
|
|
785
|
+
// A prefix that is already one compaction summary cannot get smaller by
|
|
786
|
+
// summarizing it again. Wait until newer rounds accumulate.
|
|
787
|
+
const onlyExistingSummary = foldable.length === 1 && foldable[0]?.instruction === '[history compacted]';
|
|
788
|
+
const shouldCompact = compaction !== undefined && this.compactionBoundaries.has(context.lane.resume.step) && !context.lane.resume.step.startsWith('$compact:') && context.lane.activeWaitId === undefined && sdk.compactPending !== true && pressure !== undefined && pressure.historyTokens > pressure.softTokens && context.lane.context.history.length > keepRecentRounds && !onlyExistingSummary;
|
|
666
789
|
if (shouldCompact)
|
|
667
790
|
return { actions: [], next: { programId: this.config.id, programVersion: this.config.version, step: compactSummarize, locals: { ...ordinaryLocals(context.lane.resume.locals), $sdk: { ...sdk, compactPending: true, compactReturnStep: context.lane.resume.step } } } };
|
|
668
791
|
const requestedStep = shouldCompact ? compactSummarize : context.lane.resume.step;
|
package/dist/dsl/session.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface PulseSessionSnapshot {
|
|
|
26
26
|
waits: unknown[];
|
|
27
27
|
results: unknown[];
|
|
28
28
|
mergeProposals: unknown[];
|
|
29
|
+
humanInputs: unknown[];
|
|
29
30
|
quarantine: unknown[];
|
|
30
31
|
observationsPending: number;
|
|
31
32
|
}
|
|
@@ -36,10 +37,16 @@ export declare class PulseSession {
|
|
|
36
37
|
/** Stable session handle used by the explicit warm-start API. */
|
|
37
38
|
readonly sessionId: string;
|
|
38
39
|
constructor(runtime: PulseRuntime, agentId: string);
|
|
40
|
+
private ownsAgent;
|
|
39
41
|
private ownsEvent;
|
|
42
|
+
private hasActiveDescendant;
|
|
40
43
|
stream(fromSeq?: number): AsyncIterable<SessionEvent>;
|
|
41
44
|
snapshot(): Promise<PulseSessionSnapshot>;
|
|
42
45
|
outcome(): Promise<Outcome>;
|
|
43
46
|
reply(effectId: string, value: JsonValue): Promise<void>;
|
|
47
|
+
/** Submit a human message while the agent is still running. The scheduler
|
|
48
|
+
* records it immediately; targetEffectId is optional for direct replies to
|
|
49
|
+
* a waiting Human Effect. */
|
|
50
|
+
submitHumanInput(inputId: string, value: JsonValue, targetEffectId?: string): Promise<void>;
|
|
44
51
|
cancel(reason: string): Promise<void>;
|
|
45
52
|
}
|
package/dist/dsl/session.js
CHANGED
|
@@ -19,15 +19,29 @@ export class PulseSession {
|
|
|
19
19
|
};
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
ownsAgent(agentId) {
|
|
23
|
+
let current = this.runtime.state.agents.get(agentId);
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
while (current && !seen.has(current.id)) {
|
|
26
|
+
if (current.id === this.agentId)
|
|
27
|
+
return true;
|
|
28
|
+
seen.add(current.id);
|
|
29
|
+
current = current.parentAgentId === undefined ? undefined : this.runtime.state.agents.get(current.parentAgentId);
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
22
33
|
ownsEvent(event) {
|
|
23
|
-
if (event.agentId
|
|
34
|
+
if (event.agentId !== undefined && this.ownsAgent(event.agentId))
|
|
35
|
+
return true;
|
|
36
|
+
if (event.laneId !== undefined && this.ownsAgent(this.runtime.state.lanes.get(event.laneId)?.agentId ?? ''))
|
|
37
|
+
return true;
|
|
38
|
+
if (event.effectId !== undefined && this.ownsAgent(this.runtime.state.effects.get(event.effectId)?.agentId ?? ''))
|
|
24
39
|
return true;
|
|
25
|
-
if (event.laneId !== undefined)
|
|
26
|
-
return this.runtime.state.lanes.get(event.laneId)?.agentId === this.agentId;
|
|
27
|
-
if (event.effectId !== undefined)
|
|
28
|
-
return this.runtime.state.effects.get(event.effectId)?.agentId === this.agentId;
|
|
29
40
|
return false;
|
|
30
41
|
}
|
|
42
|
+
hasActiveDescendant() {
|
|
43
|
+
return [...this.runtime.state.agents.values()].some((agent) => this.ownsAgent(agent.id) && agent.id !== this.agentId && agent.state !== undefined && !['succeeded', 'failed', 'cancelled'].includes(agent.state));
|
|
44
|
+
}
|
|
31
45
|
async *stream(fromSeq = 0) {
|
|
32
46
|
let cursor = fromSeq;
|
|
33
47
|
let observationCursor = 0;
|
|
@@ -55,9 +69,11 @@ export class PulseSession {
|
|
|
55
69
|
yield { kind: 'observation', type: 'observation', seq: observation.seq, observation: observation };
|
|
56
70
|
}
|
|
57
71
|
const root = [...this.runtime.state.lanes.values()].find((lane) => lane.agentId === this.agentId && lane.ownerLaneId === undefined);
|
|
58
|
-
if (root && ['succeeded', 'failed', 'cancelled'].includes(root.status) && (this.runtime.state.events.at(-1)?.seq ?? compactedThrough) === cursor)
|
|
72
|
+
if (root && ['succeeded', 'failed', 'cancelled'].includes(root.status) && !this.hasActiveDescendant() && (this.runtime.state.events.at(-1)?.seq ?? compactedThrough) === cursor)
|
|
59
73
|
return;
|
|
60
|
-
|
|
74
|
+
// Wait for runtime activity instead of polling with setImmediate while
|
|
75
|
+
// an external effect is still in flight.
|
|
76
|
+
await this.runtime.waitForActivity();
|
|
61
77
|
}
|
|
62
78
|
}
|
|
63
79
|
async snapshot() {
|
|
@@ -71,11 +87,12 @@ export class PulseSession {
|
|
|
71
87
|
now: this.runtime.state.now,
|
|
72
88
|
eventSeq: this.runtime.state.events.at(-1)?.seq ?? 0,
|
|
73
89
|
agent: agent ? structuredClone({ id: agent.id, goal: agent.goal ?? null, state: agent.state ?? null, latestGlobalVersion: agent.latestGlobalVersion, globalVersions: [...agent.globalVersions.entries()].map(([version, value]) => ({ version, value, ...(agent.globalPrivacy?.get(version) === undefined ? {} : { privacy: agent.globalPrivacy.get(version) }) })) }) : null,
|
|
74
|
-
lanes: [...this.runtime.state.lanes.values()].filter((lane) => laneIds.has(lane.id)).map((lane) => structuredClone({ id: lane.id, agentId: lane.agentId, ownerLaneId: lane.ownerLaneId ?? null, status: lane.status, cancelReason: lane.cancelReason ?? null, failure: lane.failure ?? null, version: lane.version, goal: lane.goal, priority: lane.priority, inheritedFloor: lane.inheritedFloor ?? null, readySince: lane.readySince, resume: lane.resume, pendingResumeInput: lane.pendingResumeInput ?? null, contextSnapshotVersion: lane.contextSnapshotVersion, context: lane.context, visibleResultRefs: lane.visibleResultRefs ? [...lane.visibleResultRefs] : [], historyPressure: lane.historyPressure ?? null, activeWaitId: lane.activeWaitId ?? null, children: [...lane.children], ownedEffectIds: [...lane.ownedEffectIds], resultRef: lane.resultRef ?? null, closingResult: lane.closingResult ?? null, consecutiveControlErrors: lane.consecutiveControlErrors ?? 0, unresolvedEffectIds: lane.unresolvedEffectIds ?? [], progressWatchdog: lane.progressWatchdog ?? null })),
|
|
90
|
+
lanes: [...this.runtime.state.lanes.values()].filter((lane) => laneIds.has(lane.id)).map((lane) => structuredClone({ id: lane.id, agentId: lane.agentId, ownerLaneId: lane.ownerLaneId ?? null, status: lane.status, cancelReason: lane.cancelReason ?? null, failure: lane.failure ?? null, version: lane.version, goal: lane.goal, priority: lane.priority, inheritedFloor: lane.inheritedFloor ?? null, readySince: lane.readySince, resume: lane.resume, pendingResumeInput: lane.pendingResumeInput ?? null, pendingHumanInputs: lane.pendingHumanInputs ?? [], contextSnapshotVersion: lane.contextSnapshotVersion, context: lane.context, visibleResultRefs: lane.visibleResultRefs ? [...lane.visibleResultRefs] : [], historyPressure: lane.historyPressure ?? null, activeWaitId: lane.activeWaitId ?? null, children: [...lane.children], ownedEffectIds: [...lane.ownedEffectIds], resultRef: lane.resultRef ?? null, closingResult: lane.closingResult ?? null, consecutiveControlErrors: lane.consecutiveControlErrors ?? 0, unresolvedEffectIds: lane.unresolvedEffectIds ?? [], progressWatchdog: lane.progressWatchdog ?? null })),
|
|
75
91
|
effects: effects.map((effect) => structuredClone(effect)),
|
|
76
92
|
waits: [...this.runtime.state.waits.values()].filter((wait) => laneIds.has(wait.laneId)).map((wait) => structuredClone(wait)),
|
|
77
93
|
results: [...this.runtime.state.results.values()].filter((result) => (result.effectId !== undefined && effectIds.has(result.effectId)) || [...this.runtime.state.lanes.values()].some((lane) => laneIds.has(lane.id) && lane.resultRef === result.id)).map((result) => structuredClone(result)),
|
|
78
94
|
mergeProposals: [...this.runtime.state.mergeProposals.values()].filter((proposal) => proposal.agentId === this.agentId).map((proposal) => structuredClone(proposal)),
|
|
95
|
+
humanInputs: [...this.runtime.state.humanInputs.values()].filter((input) => input.agentId === this.agentId).map((input) => structuredClone(input)),
|
|
79
96
|
quarantine: structuredClone(this.runtime.quarantine.snapshot().filter((entry) => effectIds.has(entry.effectId))),
|
|
80
97
|
observationsPending: this.runtime.observationInbox.snapshot().filter((observation) => observation.agentId === this.agentId).length,
|
|
81
98
|
};
|
|
@@ -83,11 +100,21 @@ export class PulseSession {
|
|
|
83
100
|
async outcome() { return this.execution; }
|
|
84
101
|
async reply(effectId, value) {
|
|
85
102
|
const effect = this.runtime.state.effects.get(effectId);
|
|
86
|
-
if (!effect || effect.agentId
|
|
103
|
+
if (!effect || !this.ownsAgent(effect.agentId))
|
|
87
104
|
throw new Error('EFFECT_NOT_OWNED');
|
|
88
105
|
if (effect.kind !== 'human' || effect.outcome)
|
|
89
106
|
throw new Error('EFFECT_NOT_REPLYABLE');
|
|
90
|
-
this.runtime.enqueueHostCommand({ type: 'reply', agentId:
|
|
107
|
+
if (!this.runtime.enqueueHostCommand({ type: 'reply', agentId: effect.agentId, effectId, value })) {
|
|
108
|
+
throw new Error('HOST_COMMAND_NOT_ENQUEUED');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Submit a human message while the agent is still running. The scheduler
|
|
112
|
+
* records it immediately; targetEffectId is optional for direct replies to
|
|
113
|
+
* a waiting Human Effect. */
|
|
114
|
+
async submitHumanInput(inputId, value, targetEffectId) {
|
|
115
|
+
if (!this.runtime.submitHumanInput(this.agentId, inputId, value, targetEffectId)) {
|
|
116
|
+
throw new Error('HOST_COMMAND_NOT_ENQUEUED');
|
|
117
|
+
}
|
|
91
118
|
}
|
|
92
119
|
async cancel(reason) { this.runtime.requestCancel(this.agentId, reason); }
|
|
93
120
|
}
|
package/dist/dsl/templates.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LaneProgramDefinition, type StepContext, type InstructionView, type NextStepTarget, type StepInputs } from './program.js';
|
|
1
|
+
import { type HistoryCompactionOptions, type LaneProgramDefinition, type StepContext, type InstructionView, type NextStepTarget, type StepInputs } from './program.js';
|
|
2
2
|
import type { LaneProgram } from '../scheduler/runtime.js';
|
|
3
3
|
import type { Outcome, JsonValue } from '../core/types.js';
|
|
4
4
|
import type { ZodTypeAny } from 'zod';
|
|
@@ -24,10 +24,7 @@ export declare function defineReActLane(config: {
|
|
|
24
24
|
prompt: string | ((calls: JsonValue, ctx: StepContext<JsonValue>) => string);
|
|
25
25
|
onDenied?: (reason: string, ctx: StepContext<JsonValue>) => NextStepTarget<JsonValue>;
|
|
26
26
|
};
|
|
27
|
-
historyCompaction?:
|
|
28
|
-
summarizeTask: string;
|
|
29
|
-
keepRecentRounds: number;
|
|
30
|
-
};
|
|
27
|
+
historyCompaction?: HistoryCompactionOptions;
|
|
31
28
|
}): LaneProgramDefinition;
|
|
32
29
|
export declare function defineSeriesLane(config: {
|
|
33
30
|
id: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './transitions/index.js';
|
|
|
7
7
|
export * from './dependencies/index.js';
|
|
8
8
|
export * from './scheduler/index.js';
|
|
9
9
|
export * from './scheduler/runtime.js';
|
|
10
|
+
export * from './scheduler/human-arbitration.js';
|
|
10
11
|
export * from './lifecycle/index.js';
|
|
11
12
|
export * from './context/index.js';
|
|
12
13
|
export * from './models/index.js';
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from './transitions/index.js';
|
|
|
7
7
|
export * from './dependencies/index.js';
|
|
8
8
|
export * from './scheduler/index.js';
|
|
9
9
|
export * from './scheduler/runtime.js';
|
|
10
|
+
export * from './scheduler/human-arbitration.js';
|
|
10
11
|
export * from './lifecycle/index.js';
|
|
11
12
|
export * from './context/index.js';
|
|
12
13
|
export * from './models/index.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { EffectRecord, HumanInputRecord, JsonValue, LaneRecord } from '../core/types.js';
|
|
2
|
+
export type HumanArbitrationAction = 'respond' | 'steer' | 'spawn' | 'defer' | 'cancel';
|
|
3
|
+
export interface HumanArbitrationCandidate {
|
|
4
|
+
laneId: string;
|
|
5
|
+
agentId: string;
|
|
6
|
+
status: LaneRecord['status'];
|
|
7
|
+
priority: number;
|
|
8
|
+
goal?: string;
|
|
9
|
+
activeWaitId?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HumanArbitrationEffectCandidate {
|
|
12
|
+
effectId: string;
|
|
13
|
+
agentId: string;
|
|
14
|
+
laneId: string;
|
|
15
|
+
kind: EffectRecord['kind'];
|
|
16
|
+
state: EffectRecord['state'];
|
|
17
|
+
sideEffectPolicy?: EffectRecord['sideEffectPolicy'];
|
|
18
|
+
sideEffectState: EffectRecord['sideEffectState'];
|
|
19
|
+
}
|
|
20
|
+
export interface HumanArbitrationRequest {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
decisionId: string;
|
|
23
|
+
agentId: string;
|
|
24
|
+
input: HumanInputRecord;
|
|
25
|
+
lanes: readonly HumanArbitrationCandidate[];
|
|
26
|
+
effects: readonly HumanArbitrationEffectCandidate[];
|
|
27
|
+
availableLLMSlots: number;
|
|
28
|
+
}
|
|
29
|
+
export interface HumanArbitrationDecision {
|
|
30
|
+
schemaVersion?: 1;
|
|
31
|
+
decisionId: string;
|
|
32
|
+
inputId: string;
|
|
33
|
+
agentId: string;
|
|
34
|
+
action: HumanArbitrationAction;
|
|
35
|
+
targetLaneId?: string;
|
|
36
|
+
targetEffectId?: string;
|
|
37
|
+
reason?: string;
|
|
38
|
+
modelId: string;
|
|
39
|
+
}
|
|
40
|
+
export interface HumanArbitrationModel {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
decide(request: HumanArbitrationRequest, signal: AbortSignal): Promise<HumanArbitrationDecision>;
|
|
43
|
+
}
|
|
44
|
+
export interface HumanArbitrationConfig {
|
|
45
|
+
model?: HumanArbitrationModel;
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
}
|
|
48
|
+
export declare class HumanArbitrationCoordinator {
|
|
49
|
+
private readonly model;
|
|
50
|
+
private readonly timeoutMs;
|
|
51
|
+
private outstanding;
|
|
52
|
+
private readonly controllers;
|
|
53
|
+
constructor(model: HumanArbitrationModel, timeoutMs: number);
|
|
54
|
+
get pending(): number;
|
|
55
|
+
request(request: HumanArbitrationRequest, onDecision: (decision: HumanArbitrationDecision) => void, onFailure: () => void): boolean;
|
|
56
|
+
cancel(): void;
|
|
57
|
+
}
|
|
58
|
+
/** Deterministic, side-effect-safe control grammar used before model arbitration. */
|
|
59
|
+
export declare function ruleHumanArbitration(value: JsonValue, agentId: string, inputId: string, modelId?: string): HumanArbitrationDecision | undefined;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export class HumanArbitrationCoordinator {
|
|
2
|
+
model;
|
|
3
|
+
timeoutMs;
|
|
4
|
+
outstanding = 0;
|
|
5
|
+
controllers = new Set();
|
|
6
|
+
constructor(model, timeoutMs) {
|
|
7
|
+
this.model = model;
|
|
8
|
+
this.timeoutMs = timeoutMs;
|
|
9
|
+
}
|
|
10
|
+
get pending() { return this.outstanding; }
|
|
11
|
+
request(request, onDecision, onFailure) {
|
|
12
|
+
if (this.outstanding > 0)
|
|
13
|
+
return false;
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
this.controllers.add(controller);
|
|
16
|
+
this.outstanding++;
|
|
17
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
18
|
+
void Promise.resolve().then(() => this.model.decide(structuredClone(request), controller.signal)).then((decision) => {
|
|
19
|
+
if (decision && typeof decision === 'object')
|
|
20
|
+
onDecision(structuredClone(decision));
|
|
21
|
+
else
|
|
22
|
+
onFailure();
|
|
23
|
+
}, () => onFailure()).finally(() => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
this.controllers.delete(controller);
|
|
26
|
+
this.outstanding--;
|
|
27
|
+
});
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
cancel() { for (const controller of this.controllers)
|
|
31
|
+
controller.abort(); this.controllers.clear(); }
|
|
32
|
+
}
|
|
33
|
+
/** Deterministic, side-effect-safe control grammar used before model arbitration. */
|
|
34
|
+
export function ruleHumanArbitration(value, agentId, inputId, modelId = 'rules') {
|
|
35
|
+
const object = value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
36
|
+
const text = typeof value === 'string' ? value.trim() : typeof object?.text === 'string' ? object.text.trim() : undefined;
|
|
37
|
+
const command = typeof object?.command === 'string' ? object.command : text?.startsWith('/') ? text.slice(1).split(/\s+/, 1)[0] : undefined;
|
|
38
|
+
const targetLaneId = typeof object?.laneId === 'string' ? object.laneId : undefined;
|
|
39
|
+
const targetEffectId = typeof object?.effectId === 'string' ? object.effectId : undefined;
|
|
40
|
+
if (!command && text) {
|
|
41
|
+
const normalizedText = text.toLowerCase().replace(/[\s,。!?!?、,.]+/g, '');
|
|
42
|
+
const isContinuation = /^(继续|继续处理|接着做|往下做|恢复任务|resume|continue|goon|keepgoing)$/.test(normalizedText);
|
|
43
|
+
const isStatusCheck = /^(你还活着吗|还在吗|有进展吗|进展呢|现在怎么样|areyoualive|areyoustillthere|anyupdate|status)$/.test(normalizedText);
|
|
44
|
+
if (isContinuation || isStatusCheck) {
|
|
45
|
+
return { schemaVersion: 1, decisionId: `rule:${inputId}`, inputId, agentId, action: 'steer', ...(targetLaneId === undefined ? {} : { targetLaneId }), reason: isContinuation ? 'Human asked the current task to continue.' : 'Human asked for the current task status.', modelId };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!command)
|
|
49
|
+
return undefined;
|
|
50
|
+
const normalized = command.toLowerCase();
|
|
51
|
+
if (normalized === 'cancel' || normalized === 'stop' || normalized === 'abort')
|
|
52
|
+
return { schemaVersion: 1, decisionId: `rule:${inputId}`, inputId, agentId, action: 'cancel', ...(targetLaneId === undefined ? {} : { targetLaneId }), ...(targetEffectId === undefined ? {} : { targetEffectId }), reason: 'Human requested cancellation.', modelId };
|
|
53
|
+
if (normalized === 'steer' || normalized === 'redirect')
|
|
54
|
+
return { schemaVersion: 1, decisionId: `rule:${inputId}`, inputId, agentId, action: 'steer', ...(targetLaneId === undefined ? {} : { targetLaneId }), reason: 'Human requested steering.', modelId };
|
|
55
|
+
if (normalized === 'spawn' || normalized === 'parallel')
|
|
56
|
+
return { schemaVersion: 1, decisionId: `rule:${inputId}`, inputId, agentId, action: 'spawn', reason: 'Human requested a concurrent interaction.', modelId };
|
|
57
|
+
if (normalized === 'defer' || normalized === 'later')
|
|
58
|
+
return { schemaVersion: 1, decisionId: `rule:${inputId}`, inputId, agentId, action: 'defer', reason: 'Human requested deferral.', modelId };
|
|
59
|
+
if (normalized === 'respond' || normalized === 'reply')
|
|
60
|
+
return { schemaVersion: 1, decisionId: `rule:${inputId}`, inputId, agentId, action: 'respond', ...(targetEffectId === undefined ? {} : { targetEffectId }), modelId };
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|