@sema-agent/core 7.10.0 → 7.11.1
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 +90 -0
- package/dist/agents/child-model-seat.d.ts +45 -18
- package/dist/agents/child-model-seat.js +12 -8
- package/dist/agents/subagent.js +6 -5
- package/dist/agents/teacher.js +2 -2
- package/dist/core/auto-mode-defaults.d.ts +19 -3
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +24 -20
- package/dist/core/auto-mode.js +12 -12
- package/dist/core/gate-fold.js +1 -0
- package/dist/core/gate-lanes.d.ts +6 -1
- package/dist/core/gate-lanes.js +45 -18
- package/dist/core/governance-codes.d.ts +1 -0
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/hooks.d.ts +13 -0
- package/dist/core/permission-rule-model.d.ts +5 -3
- package/dist/core/permission-rule-model.js +7 -3
- package/dist/core/persisted-rule-arms.js +4 -3
- package/dist/core/read-only-shell-table.d.ts +87 -0
- package/dist/core/read-only-shell-table.js +485 -0
- package/dist/core/read-only-shell.d.ts +42 -0
- package/dist/core/read-only-shell.js +316 -0
- package/dist/core/roles.d.ts +3 -2
- package/dist/core/runner/contracts.d.ts +70 -0
- package/dist/core/runner/gate-exit.d.ts +5 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +38 -12
- package/dist/core/runner/prepare-gate-stations.js +9 -0
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/prepare-turn-wiring.js +1 -1
- package/dist/core/runner/runtask.js +34 -510
- package/dist/core/runner/stream-halt-verbs.d.ts +38 -0
- package/dist/core/runner/stream-halt-verbs.js +82 -0
- package/dist/core/runner/stream-lifecycle-verbs.d.ts +34 -0
- package/dist/core/runner/stream-lifecycle-verbs.js +126 -0
- package/dist/core/runner/stream-reap.d.ts +30 -0
- package/dist/core/runner/stream-reap.js +40 -0
- package/dist/core/runner/stream-settle-backstop.d.ts +38 -0
- package/dist/core/runner/stream-settle-backstop.js +113 -0
- package/dist/core/runner/stream-steer-verb.d.ts +30 -0
- package/dist/core/runner/stream-steer-verb.js +185 -0
- package/dist/core/shell-lexer.d.ts +18 -0
- package/dist/core/shell-lexer.js +17 -10
- package/dist/core/shell-wrapper-table.js +8 -5
- package/dist/core/tool-policy.d.ts +4 -1
- package/dist/core/tool-policy.js +1 -1
- package/dist/core/tools.d.ts +28 -7
- package/dist/core/tools.js +44 -4
- package/dist/core/trace.d.ts +15 -0
- package/dist/engine/harness/agent-harness.d.ts +3 -1
- package/dist/engine/harness/agent-harness.js +1 -1
- package/dist/engine/harness/types.d.ts +4 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/orchestration/run-workflow-tool.d.ts +5 -2
- package/dist/orchestration/run-workflow-tool.js +2 -1
- package/dist/orchestration/workflow-governance.d.ts +3 -2
- package/dist/orchestration/workflow-primitives.d.ts +4 -1
- package/dist/orchestration/workflow-primitives.js +1 -6
- package/dist/orchestration/workflow.d.ts +12 -4
- package/dist/orchestration/workflow.js +24 -7
- package/dist/prompt-assembly/turn-snapshot.d.ts +4 -2
- package/dist/tools/fs/bash-readonly-classifier.d.ts +5 -0
- package/dist/tools/fs/bash-readonly-classifier.js +1 -0
- package/dist/tools/fs/fs-bash.js +3 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +35 -1
|
@@ -4,7 +4,7 @@ import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNoti
|
|
|
4
4
|
import { planRejectionClears, resolveTriggerWindow } from "../context-edit.js";
|
|
5
5
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, isSyntheticApiErrorMessage, uuidv7 } from "../../internal/harness.js";
|
|
6
6
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
7
|
-
import { CheckpointError, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, resolveCheckpointStore,
|
|
7
|
+
import { CheckpointError, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, resolveCheckpointStore, realApprovalOrgFact } from "../checkpoint-store.js";
|
|
8
8
|
import { GIT_STATUS_ECHO_PREVIEW, stripGitStatusUnits } from "./git-status-frame.js";
|
|
9
9
|
import { settleExecutionRecord } from "./execution-record.js";
|
|
10
10
|
import { engineVersion } from "../version.js";
|
|
@@ -46,7 +46,7 @@ import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
|
46
46
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
47
47
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
48
48
|
import { formatHookFeedback, hookSeatExpiredError, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
49
|
-
import { buildHumanInputEvent,
|
|
49
|
+
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
50
50
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
51
51
|
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
52
52
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
@@ -54,11 +54,11 @@ import { PAUSE_REGISTRY } from "../pause-registry.js";
|
|
|
54
54
|
import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
|
|
55
55
|
import { mintGateOutcome } from "./gate-exit.js";
|
|
56
56
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
57
|
-
import { discloseDroppedPending, isDelegatedAgentTerminal,
|
|
57
|
+
import { discloseDroppedPending, isDelegatedAgentTerminal, isTerminalTaskNotification, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
58
58
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
59
59
|
import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
|
|
60
60
|
import { createRunState } from "./initial-run-state.js";
|
|
61
|
-
import { nextHumanInputSeq
|
|
61
|
+
import { nextHumanInputSeq } from "./steer-admission.js";
|
|
62
62
|
import { reconciledToolEndBody, toolEndBodyFrom, toolResultMsg, writeFamilyOfCanonical } from "./tool-end-body.js";
|
|
63
63
|
import { answerFaceForRedeemedCall, deepJsonEqual, DEFERRED_REISSUE, pendingContentAskCallId, resumeContinuation, resumeDecisionWasNegative } from "./decide-continuation.js";
|
|
64
64
|
import { awaitChargeWithSlowDisclosure, DEFAULT_PRECALL_OUTPUT_TOKENS, discloseUnevaluableWindow, platformLimitTerminal, resolveMaxTurns, startTimeout, TIMER_LATENESS_REPORT_MS } from "./clock-and-limits.js";
|
|
@@ -73,6 +73,11 @@ import { resumeInternalsAndConfig } from "./resume-internals-and-config.js";
|
|
|
73
73
|
import { resumeApply } from "./resume-apply.js";
|
|
74
74
|
import { resumePreflight } from "./resume-preflight.js";
|
|
75
75
|
import { resumeClaim } from "./resume-claim.js";
|
|
76
|
+
import { streamSettleBackstop } from "./stream-settle-backstop.js";
|
|
77
|
+
import { streamReap } from "./stream-reap.js";
|
|
78
|
+
import { streamSteerVerb } from "./stream-steer-verb.js";
|
|
79
|
+
import { streamLifecycleVerbs } from "./stream-lifecycle-verbs.js";
|
|
80
|
+
import { streamHaltVerbs } from "./stream-halt-verbs.js";
|
|
76
81
|
const STOP_HOOK_BLOCK_CAP = 8;
|
|
77
82
|
const ORG_DISCLOSURE_MAX_CHARS = 600;
|
|
78
83
|
const SUGGESTIONS_DEFAULT_COUNT = 3;
|
|
@@ -308,7 +313,6 @@ export class Runner {
|
|
|
308
313
|
return Promise.race([p, timeout]).finally(() => clearTimeout(t));
|
|
309
314
|
};
|
|
310
315
|
let reapHandle;
|
|
311
|
-
let steerChain = Promise.resolve();
|
|
312
316
|
const acceptedSteerInputs = new Map();
|
|
313
317
|
const notifyRef = {};
|
|
314
318
|
const captureOptOutRef = {};
|
|
@@ -363,149 +367,29 @@ export class Runner {
|
|
|
363
367
|
drainManualCompactWaiters("mooted");
|
|
364
368
|
}
|
|
365
369
|
})();
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
catch (reopenErr) {
|
|
378
|
-
this.parentConstraintRegistry.delete(resume.cp.token);
|
|
379
|
-
const original = err instanceof Error ? err : new Error(String(err));
|
|
380
|
-
const reopenMsg = reopenErr instanceof Error ? reopenErr.message : String(reopenErr);
|
|
381
|
-
const definitive = errorCodeOf(reopenErr) === "checkpoint.reopen_failed";
|
|
382
|
-
reopenStateUnknown = !definitive;
|
|
383
|
-
err = Object.assign(new Error(definitive
|
|
384
|
-
? `${reopenMsg} (original resume failure: ${original.message})`
|
|
385
|
-
: `checkpoint reopen threw mid-flight — state UNKNOWN, confirm via the checkpoint store before retrying: ${reopenMsg} (original resume failure: ${original.message})`, { cause: original }), { code: "checkpoint.reopen_failed" });
|
|
386
|
-
code = "checkpoint.reopen_failed";
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (resume)
|
|
390
|
-
this.locallyClaimedTokens.delete(resume.cp.token);
|
|
391
|
-
if (resume && !reopenCommitted) {
|
|
392
|
-
this.parentConstraintRegistry.delete(resume.cp.token);
|
|
393
|
-
await settleTeardownLeg(() => this.sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resume-failure leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
|
|
394
|
-
if (!reopenStateUnknown) {
|
|
395
|
-
const failedReap = this.suspendedEnvReaps.get(resume.cp.token);
|
|
396
|
-
this.suspendedEnvReaps.delete(resume.cp.token);
|
|
397
|
-
if (failedReap?.env !== undefined && hasDestroy(failedReap.env)) {
|
|
398
|
-
const failedEnv = failedReap.env;
|
|
399
|
-
await settleTeardownLeg(() => failedEnv.destroy(), "terminal resume-failure env destroy", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (resultValue !== undefined) {
|
|
404
|
-
try {
|
|
405
|
-
this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId: resultValue.sessionId });
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
}
|
|
409
|
-
queue.close();
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
const remoteEnvFailure = (() => {
|
|
413
|
-
let cur = err;
|
|
414
|
-
for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
|
|
415
|
-
const note = cur.remoteEnvFailure;
|
|
416
|
-
if (note !== undefined)
|
|
417
|
-
return [note];
|
|
418
|
-
cur = cur.cause;
|
|
419
|
-
}
|
|
420
|
-
return undefined;
|
|
421
|
-
})();
|
|
422
|
-
resultValue = {
|
|
423
|
-
taskId: taskIdRef.current ?? "unknown",
|
|
424
|
-
sessionId: taskIdRef.sessionId ?? "unknown",
|
|
425
|
-
...(taskIdRef.runId !== undefined ? { runId: taskIdRef.runId } : {}),
|
|
426
|
-
terminal: { kind: "failed", ...(code !== undefined ? { code } : {}), message: err instanceof Error ? err.message : String(err) },
|
|
427
|
-
result: "",
|
|
428
|
-
...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
|
|
429
|
-
...(taskIdRef.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: taskIdRef.effectiveMemoryScopes } : {}),
|
|
430
|
-
...(taskIdRef.effectiveReasoning !== undefined ? { effectiveReasoning: taskIdRef.effectiveReasoning } : {}),
|
|
431
|
-
...(() => {
|
|
432
|
-
const observed = taskIdRef.editedFiles?.();
|
|
433
|
-
return observed !== undefined && observed.length > 0 ? { editedFiles: observed } : {};
|
|
434
|
-
})(),
|
|
435
|
-
...(() => {
|
|
436
|
-
const hinted = err.retryAfterMs;
|
|
437
|
-
return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
|
|
438
|
-
? { retryAfterMs: hinted }
|
|
439
|
-
: {};
|
|
440
|
-
})(),
|
|
441
|
-
stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
|
|
442
|
-
};
|
|
443
|
-
emitTrace(entryTracer.tracer, () => ({
|
|
444
|
-
kind: "task.end",
|
|
445
|
-
version: 1,
|
|
446
|
-
taskId: taskIdRef.current ?? "unknown",
|
|
447
|
-
...(taskIdRef.runId !== undefined ? { runId: taskIdRef.runId } : {}),
|
|
448
|
-
status: "failed",
|
|
449
|
-
errorCode: code,
|
|
450
|
-
turns: 0,
|
|
451
|
-
tokens: 0,
|
|
452
|
-
durationMs: Date.now() - runStartedAt,
|
|
453
|
-
ts: Date.now(),
|
|
454
|
-
}));
|
|
455
|
-
const owedTerminal = taskIdRef.delegationTerminalOwed;
|
|
456
|
-
if (owedTerminal !== undefined) {
|
|
457
|
-
taskIdRef.delegationTerminalOwed = undefined;
|
|
458
|
-
deliverDelegationLifecycle(this.deps.onDelegationLifecycle, { phase: "terminal", identity: owedTerminal, status: "failed", turns: 0, ...(code !== undefined ? { errorCode: code } : {}) }, createSafeNotifier({
|
|
459
|
-
onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
460
|
-
}), "runtask.onDelegationLifecycle");
|
|
461
|
-
}
|
|
462
|
-
if (forwardsSubagentEvents(spec))
|
|
463
|
-
await drainForwardedFramesBeforeDone();
|
|
464
|
-
queue.push({ type: "done", result: resultValue });
|
|
465
|
-
queue.close();
|
|
466
|
-
});
|
|
370
|
+
const live = {
|
|
371
|
+
get resultValue() { return resultValue; },
|
|
372
|
+
set resultValue(v) { resultValue = v; },
|
|
373
|
+
get handle() { return handle; },
|
|
374
|
+
get reapHandle() { return reapHandle; },
|
|
375
|
+
};
|
|
376
|
+
const settled = run.catch(streamSettleBackstop({
|
|
377
|
+
spec, resume, queue, taskIdRef, entryTracer, runStartedAt, live, runner: this.depsSeat, sessions: this.sessions,
|
|
378
|
+
locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry, suspendedEnvReaps: this.suspendedEnvReaps,
|
|
379
|
+
}).onRunRejected);
|
|
467
380
|
void settled.then(() => acceptedSteerInputs.clear(), () => acceptedSteerInputs.clear());
|
|
468
381
|
const steeringError = (msg, code = "steering.not_running") => {
|
|
469
382
|
const e = new Error(`cannot steer: ${msg}`);
|
|
470
383
|
e.code = code;
|
|
471
384
|
return e;
|
|
472
385
|
};
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
this.deps.onError?.(err, { phase: "config", sessionId: rh.sessionId });
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
}
|
|
484
|
-
};
|
|
485
|
-
let won;
|
|
486
|
-
try {
|
|
487
|
-
won = await rh.store.expire(rh.token, rh.scope);
|
|
488
|
-
}
|
|
489
|
-
catch (err) {
|
|
490
|
-
reportErr(err);
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
if (!won) {
|
|
494
|
-
if (!this.locallyClaimedTokens.has(rh.token))
|
|
495
|
-
this.suspendedEnvReaps.delete(rh.token);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
this.parentConstraintRegistry.delete(rh.token);
|
|
499
|
-
this.suspendedEnvReaps.delete(rh.token);
|
|
500
|
-
if (rh.env && hasDestroy(rh.env)) {
|
|
501
|
-
try {
|
|
502
|
-
await rh.env.destroy();
|
|
503
|
-
}
|
|
504
|
-
catch (err) {
|
|
505
|
-
reportErr(err);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
};
|
|
386
|
+
const { reapSuspended } = streamReap({
|
|
387
|
+
live, runner: this.depsSeat,
|
|
388
|
+
locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry, suspendedEnvReaps: this.suspendedEnvReaps,
|
|
389
|
+
});
|
|
390
|
+
const { steer } = streamSteerVerb({ spec, internals, queue, acceptedSteerInputs, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, steeringError, runner: this.depsSeat });
|
|
391
|
+
const { notify, optOutMemoryCapture, compact, detach, interrupt } = streamLifecycleVerbs({ live, ready, orTimeout, steeringError, notifyRef, captureOptOutRef, manualCompactRef, detachHub });
|
|
392
|
+
const { halt, destroy } = streamHaltVerbs({ spec, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, run, reapSuspended, runner: this.depsSeat });
|
|
509
393
|
return {
|
|
510
394
|
[Symbol.asyncIterator]: () => queue[Symbol.asyncIterator](),
|
|
511
395
|
result: async () => {
|
|
@@ -516,374 +400,14 @@ export class Runner {
|
|
|
516
400
|
await run.catch(() => { });
|
|
517
401
|
return suggestionsDone.catch(() => []);
|
|
518
402
|
},
|
|
519
|
-
steer
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
throw steeringError("inputId must be a string when supplied", "steering.invalid_content");
|
|
528
|
-
}
|
|
529
|
-
if (inputId === "" || inputId.length > MAX_STEER_INPUT_ID_CHARS) {
|
|
530
|
-
throw steeringError(`inputId must be a non-empty string of at most ${MAX_STEER_INPUT_ID_CHARS} characters`, "steering.invalid_content");
|
|
531
|
-
}
|
|
532
|
-
if (inputId === LEGACY_PENDING_STEER_INPUT_ID) {
|
|
533
|
-
throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
const priorityIn = options?.priority;
|
|
537
|
-
if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
|
|
538
|
-
throw steeringError(`priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when supplied`, "steering.invalid_content");
|
|
539
|
-
}
|
|
540
|
-
const priority = priorityIn ?? "next";
|
|
541
|
-
const actorIn = options?.actor;
|
|
542
|
-
const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
|
|
543
|
-
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
544
|
-
const effectiveInputId = typeof inputId === "string" ? inputId : uuidv7();
|
|
545
|
-
const parkRecord = {
|
|
546
|
-
text,
|
|
547
|
-
trusted,
|
|
548
|
-
inputId: effectiveInputId,
|
|
549
|
-
...(priorityIn !== undefined ? { priority } : {}),
|
|
550
|
-
...(actor !== undefined ? { actor } : {}),
|
|
551
|
-
};
|
|
552
|
-
let payload;
|
|
553
|
-
let mintsAFrame;
|
|
554
|
-
let replay;
|
|
555
|
-
const noteAccepted = (h) => {
|
|
556
|
-
if (!mintsAFrame)
|
|
557
|
-
return;
|
|
558
|
-
if (typeof inputId === "string")
|
|
559
|
-
acceptedSteerInputs.set(inputId, replay);
|
|
560
|
-
queue.push({
|
|
561
|
-
...buildHumanInputEvent({
|
|
562
|
-
carrier: "steer",
|
|
563
|
-
source: "steer",
|
|
564
|
-
delivery: "queued",
|
|
565
|
-
sessionSeq: nextHumanInputSeq(h.harness),
|
|
566
|
-
inputId: effectiveInputId,
|
|
567
|
-
...(actor !== undefined ? { actor } : {}),
|
|
568
|
-
...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
|
|
569
|
-
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
570
|
-
}),
|
|
571
|
-
eventId: uuidv7(),
|
|
572
|
-
...(internals?.parentToolCallId !== undefined
|
|
573
|
-
? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) }
|
|
574
|
-
: {}),
|
|
575
|
-
});
|
|
576
|
-
};
|
|
577
|
-
const deliver = async () => {
|
|
578
|
-
if (resultValue)
|
|
579
|
-
throw steeringError("the task has already finished");
|
|
580
|
-
const h = handle ?? (await orTimeout(ready));
|
|
581
|
-
if (!h)
|
|
582
|
-
throw steeringError("the task is not running");
|
|
583
|
-
payload = trusted ? formatHookFeedback(projected, h.reminderMark) : frameMidTurnUserInput(projected);
|
|
584
|
-
mintsAFrame = payload.trim().length !== 0;
|
|
585
|
-
replay = { payload, trusted, priority, ...(actor !== undefined ? { actor } : {}) };
|
|
586
|
-
if (typeof inputId === "string") {
|
|
587
|
-
const prior = acceptedSteerInputs.get(inputId);
|
|
588
|
-
if (prior !== undefined) {
|
|
589
|
-
if (resultValue !== undefined || h.loop.ended)
|
|
590
|
-
throw steeringError("the task is no longer running");
|
|
591
|
-
if (!sameAcceptedSteerInput(prior, replay)) {
|
|
592
|
-
throw steeringError("a different steering instruction was already accepted under this inputId — re-issue this one with a fresh inputId " +
|
|
593
|
-
"(an identical payload would have been an idempotent retry)", "steering.duplicate_input_id");
|
|
594
|
-
}
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
if (resultValue !== undefined || h.loop.ended)
|
|
599
|
-
throw steeringError("the task is no longer running");
|
|
600
|
-
if (mintsAFrame) {
|
|
601
|
-
const screen = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
602
|
-
if (screen !== undefined) {
|
|
603
|
-
let decision;
|
|
604
|
-
try {
|
|
605
|
-
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: h.hookTimeoutMs, signal: h.abortController.signal, abortEnds: true }, (sig) => screen(text, { identity: h.hookIdentity, signal: sig, source: "steer", inputId: effectiveInputId, ...(actor !== undefined ? { actor: snapshotActorAssertion(actor) } : {}) }));
|
|
606
|
-
if (seat.expired) {
|
|
607
|
-
if (seat.cause === "timeout") {
|
|
608
|
-
try {
|
|
609
|
-
this.deps.onError?.(hookSeatExpiredError("userPromptSubmit", h.hookTimeoutMs, seat.cause, "the steering input was NOT accepted (fail-closed) and the caller was refused typed"), { phase: "hook", sessionId: h.sessionId });
|
|
610
|
-
}
|
|
611
|
-
catch {
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
throw steeringError(seat.cause === "timeout"
|
|
615
|
-
? `the deployment's userPromptSubmit hook did not answer within its ${h.hookTimeoutMs}ms bound while screening this steering input; the input was NOT accepted (fail-closed)`
|
|
616
|
-
: `the task was cancelled while the deployment's userPromptSubmit hook was still screening this steering input; the input was NOT accepted (fail-closed)`, "steering.blocked_by_hook");
|
|
617
|
-
}
|
|
618
|
-
decision = seat.value;
|
|
619
|
-
}
|
|
620
|
-
catch (hookErr) {
|
|
621
|
-
if (hookErr instanceof Error && hookErr.code === "steering.blocked_by_hook")
|
|
622
|
-
throw hookErr;
|
|
623
|
-
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
624
|
-
try {
|
|
625
|
-
this.deps.onError?.(err, { phase: "hook", sessionId: h.sessionId });
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
}
|
|
629
|
-
throw steeringError(`the deployment's userPromptSubmit hook crashed while screening this steering input (${inlineUntrusted(err.message)}); the input was NOT accepted (fail-closed)`, "steering.blocked_by_hook");
|
|
630
|
-
}
|
|
631
|
-
if (decision?.block !== undefined && decision.block !== "") {
|
|
632
|
-
throw steeringError(`the deployment's userPromptSubmit hook blocked this steering input: ${inlineUntrusted(decision.block)}`, "steering.blocked_by_hook");
|
|
633
|
-
}
|
|
634
|
-
if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
|
|
635
|
-
payload = `${formatHookFeedback(decision.additionalContext, h.reminderMark)}\n\n${payload}`;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
const injectFramed = async () => {
|
|
640
|
-
const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
|
|
641
|
-
if (priority === "later") {
|
|
642
|
-
await h.harness.followUp(payload, noteOptions);
|
|
643
|
-
noteAccepted(h);
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
const frame = await h.harness.steer(payload, { ...noteOptions, ...(priority === "now" ? { immediate: true } : {}) });
|
|
647
|
-
noteAccepted(h);
|
|
648
|
-
if (priority === "now" && frame !== undefined && h.harness.interruptTurn(frame)) {
|
|
649
|
-
deliverEngineNotice(this.deps.onNotice, {
|
|
650
|
-
code: "task.turn_interrupted",
|
|
651
|
-
message: "a caller-provenance steer with priority \"now\" interrupted the running turn: in-flight work was cut at " +
|
|
652
|
-
"a manufactured boundary (finished tool calls keep their real results; never-started ones settle as " +
|
|
653
|
-
"interrupted) and the run continues with the steer at the queue head.",
|
|
654
|
-
detail: {
|
|
655
|
-
inputId: effectiveInputId,
|
|
656
|
-
sessionId: h.sessionId,
|
|
657
|
-
runId: h.runId,
|
|
658
|
-
...(actor?.id !== undefined ? { actorId: actor.id } : {}),
|
|
659
|
-
...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
|
|
660
|
-
},
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
try {
|
|
665
|
-
await injectFramed();
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
catch (e) {
|
|
669
|
-
if (!(e instanceof Error && e.code === "invalid_state"))
|
|
670
|
-
throw e;
|
|
671
|
-
}
|
|
672
|
-
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
673
|
-
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
674
|
-
try {
|
|
675
|
-
await injectFramed();
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
catch (e2) {
|
|
679
|
-
if (!(e2 instanceof Error && e2.code === "invalid_state"))
|
|
680
|
-
throw e2;
|
|
681
|
-
}
|
|
682
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
683
|
-
}
|
|
684
|
-
throw steeringError("the task is no longer running");
|
|
685
|
-
};
|
|
686
|
-
const p = steerChain.then(deliver);
|
|
687
|
-
steerChain = p.then(() => undefined, () => undefined);
|
|
688
|
-
return p;
|
|
689
|
-
},
|
|
690
|
-
notify: async (input, opts) => {
|
|
691
|
-
const notifyError = (msg, code) => {
|
|
692
|
-
const e = new Error(`cannot notify: ${msg}`);
|
|
693
|
-
e.code = code;
|
|
694
|
-
return e;
|
|
695
|
-
};
|
|
696
|
-
if (typeof input?.task_id !== "string" || input.task_id.trim() === "") {
|
|
697
|
-
throw notifyError("input.task_id must be a non-empty string", "notify.invalid_payload");
|
|
698
|
-
}
|
|
699
|
-
const VALID_STATUSES = new Set(["completed", "failed", "killed", "cancelled", "event"]);
|
|
700
|
-
if (!VALID_STATUSES.has(input.status)) {
|
|
701
|
-
throw notifyError(`input.status must be one of ${[...VALID_STATUSES].join("/")}`, "notify.invalid_payload");
|
|
702
|
-
}
|
|
703
|
-
if (typeof input.summary !== "string" || input.summary.trim() === "") {
|
|
704
|
-
throw notifyError("input.summary must be a non-empty string", "notify.invalid_payload");
|
|
705
|
-
}
|
|
706
|
-
if (input.result !== undefined && typeof input.result !== "string") {
|
|
707
|
-
throw notifyError("input.result must be a string when present", "notify.invalid_payload");
|
|
708
|
-
}
|
|
709
|
-
if (input.seq !== undefined && (typeof input.seq !== "number" || !Number.isFinite(input.seq))) {
|
|
710
|
-
throw notifyError("input.seq must be a finite number when present", "notify.invalid_payload");
|
|
711
|
-
}
|
|
712
|
-
if (input.source !== undefined && typeof input.source !== "string") {
|
|
713
|
-
throw notifyError("input.source must be a string when present", "notify.invalid_payload");
|
|
714
|
-
}
|
|
715
|
-
const priorityIn = opts?.priority;
|
|
716
|
-
if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
|
|
717
|
-
throw notifyError(`opts.priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when present`, "notify.invalid_payload");
|
|
718
|
-
}
|
|
719
|
-
const priority = priorityIn;
|
|
720
|
-
if (priority === "now") {
|
|
721
|
-
throw notifyError('priority "now" (turn interrupt) is not a notification-lane power — it belongs to the steer face; use "next" for earliest-boundary delivery', "notify.invalid_priority");
|
|
722
|
-
}
|
|
723
|
-
const payload = {
|
|
724
|
-
task_id: input.task_id,
|
|
725
|
-
task_type: "external",
|
|
726
|
-
status: input.status,
|
|
727
|
-
summary: input.summary,
|
|
728
|
-
...(input.result !== undefined ? { result: input.result } : {}),
|
|
729
|
-
...(input.seq !== undefined ? { seq: input.seq } : {}),
|
|
730
|
-
...(input.source !== undefined ? { source: input.source } : {}),
|
|
731
|
-
};
|
|
732
|
-
const h = handle ?? (await orTimeout(ready));
|
|
733
|
-
if (!h || notifyRef.inject === undefined) {
|
|
734
|
-
throw notifyError("the task is not running", "notify.not_running");
|
|
735
|
-
}
|
|
736
|
-
notifyRef.inject(payload, priority !== undefined ? { priority } : undefined);
|
|
737
|
-
},
|
|
738
|
-
optOutMemoryCapture: async (options) => {
|
|
739
|
-
const reasonIn = options?.reason;
|
|
740
|
-
if (reasonIn !== undefined && typeof reasonIn !== "string") {
|
|
741
|
-
const e = new Error(`cannot opt out of memory capture: options.reason must be a string when present`);
|
|
742
|
-
e.code = "steering.invalid_content";
|
|
743
|
-
throw e;
|
|
744
|
-
}
|
|
745
|
-
const reason = reasonIn;
|
|
746
|
-
if (resultValue)
|
|
747
|
-
throw steeringError("the task has already finished");
|
|
748
|
-
const h = handle ?? (await orTimeout(ready));
|
|
749
|
-
if (!h)
|
|
750
|
-
throw steeringError("the task is not running");
|
|
751
|
-
if (captureOptOutRef.flip === undefined) {
|
|
752
|
-
const e = new Error(`cannot opt out of memory capture: this run mounted no memory session (no memory spec / no backend / prepare degraded memory-less) — there is nothing to opt out of`);
|
|
753
|
-
e.code = "memory.capture_optout_unavailable";
|
|
754
|
-
throw e;
|
|
755
|
-
}
|
|
756
|
-
return captureOptOutRef.flip(reason);
|
|
757
|
-
},
|
|
758
|
-
compact: async (opts) => {
|
|
759
|
-
if (resultValue)
|
|
760
|
-
throw steeringError("the task has already finished");
|
|
761
|
-
if (opts?.signal?.aborted)
|
|
762
|
-
return "mooted";
|
|
763
|
-
const h = handle ?? (await orTimeout(ready));
|
|
764
|
-
if (!h)
|
|
765
|
-
throw steeringError("the task is not running");
|
|
766
|
-
if (opts?.signal?.aborted)
|
|
767
|
-
return "mooted";
|
|
768
|
-
if (manualCompactRef.closed)
|
|
769
|
-
return "mooted";
|
|
770
|
-
return new Promise((resolve) => {
|
|
771
|
-
const signal = opts?.signal;
|
|
772
|
-
const entry = {
|
|
773
|
-
resolve,
|
|
774
|
-
...(signal !== undefined ? { signal } : {}),
|
|
775
|
-
...(opts?.instructions !== undefined ? { instructions: opts.instructions } : {}),
|
|
776
|
-
};
|
|
777
|
-
if (signal !== undefined) {
|
|
778
|
-
const onAbort = () => {
|
|
779
|
-
const i = manualCompactRef.waiters.indexOf(entry);
|
|
780
|
-
if (i >= 0) {
|
|
781
|
-
manualCompactRef.waiters.splice(i, 1);
|
|
782
|
-
if (manualCompactRef.waiters.length === 0) {
|
|
783
|
-
manualCompactRef.requested = false;
|
|
784
|
-
}
|
|
785
|
-
manualCompactRef.emitMooted?.("cancelled");
|
|
786
|
-
entry.resolve("mooted");
|
|
787
|
-
}
|
|
788
|
-
};
|
|
789
|
-
entry.resolve = (outcome) => {
|
|
790
|
-
signal.removeEventListener("abort", onAbort);
|
|
791
|
-
resolve(outcome);
|
|
792
|
-
};
|
|
793
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
794
|
-
}
|
|
795
|
-
manualCompactRef.requested = true;
|
|
796
|
-
manualCompactRef.waiters.push(entry);
|
|
797
|
-
});
|
|
798
|
-
},
|
|
799
|
-
detach: (toolCallId) => {
|
|
800
|
-
detachHub.request(toolCallId);
|
|
801
|
-
},
|
|
802
|
-
interrupt: async () => {
|
|
803
|
-
const h = handle ?? (await orTimeout(ready));
|
|
804
|
-
if (!h)
|
|
805
|
-
return;
|
|
806
|
-
if (!h.abortController.signal.aborted)
|
|
807
|
-
h.loop.userInterrupted = true;
|
|
808
|
-
h.abortController.abort();
|
|
809
|
-
void h.harness.abort();
|
|
810
|
-
},
|
|
811
|
-
halt: async () => {
|
|
812
|
-
const haltRefused = (msg) => {
|
|
813
|
-
const e = new Error(`cannot halt: ${msg}`);
|
|
814
|
-
e.code = "steering.not_running";
|
|
815
|
-
return e;
|
|
816
|
-
};
|
|
817
|
-
if (resultValue)
|
|
818
|
-
throw haltRefused("the task has already finished");
|
|
819
|
-
const h = handle ?? (await orTimeout(ready));
|
|
820
|
-
if (!h)
|
|
821
|
-
throw haltRefused("the task is not running");
|
|
822
|
-
const apply = () => {
|
|
823
|
-
const abortOwnedBeforeHalt = h.abortController.signal.aborted;
|
|
824
|
-
const receipt = h.harness.halt();
|
|
825
|
-
if (receipt.accepted && !abortOwnedBeforeHalt) {
|
|
826
|
-
h.loop.userHalted = true;
|
|
827
|
-
}
|
|
828
|
-
else {
|
|
829
|
-
deliverEngineNotice(this.deps.onNotice, {
|
|
830
|
-
code: "task.halt_unconsumed",
|
|
831
|
-
message: "a user halt arrived while the run was already ending for its own reason: nothing was cut or stopped " +
|
|
832
|
-
"by it — the run's own ending stands, and the result will not carry haltedByUser for this halt.",
|
|
833
|
-
detail: {
|
|
834
|
-
sessionId: h.sessionId,
|
|
835
|
-
runId: h.runId,
|
|
836
|
-
...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
|
|
837
|
-
},
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
if (receipt.turnCut) {
|
|
841
|
-
deliverEngineNotice(this.deps.onNotice, {
|
|
842
|
-
code: "task.turn_interrupted",
|
|
843
|
-
message: "a bare user halt cut the running turn: in-flight work was cut at a manufactured boundary " +
|
|
844
|
-
"(finished tool calls keep their real results; never-started ones settle as interrupted) and " +
|
|
845
|
-
"the run is collecting to a clean, resumable stop — no further model turn will start.",
|
|
846
|
-
detail: {
|
|
847
|
-
cause: "user_halt",
|
|
848
|
-
sessionId: h.sessionId,
|
|
849
|
-
runId: h.runId,
|
|
850
|
-
...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
|
|
851
|
-
},
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
return { turnCut: receipt.turnCut };
|
|
855
|
-
};
|
|
856
|
-
try {
|
|
857
|
-
if (resultValue !== undefined || h.loop.ended)
|
|
858
|
-
throw haltRefused("the task is no longer running");
|
|
859
|
-
return apply();
|
|
860
|
-
}
|
|
861
|
-
catch (e) {
|
|
862
|
-
if (!(e instanceof Error && e.code === "invalid_state"))
|
|
863
|
-
throw e;
|
|
864
|
-
}
|
|
865
|
-
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
866
|
-
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
867
|
-
try {
|
|
868
|
-
return apply();
|
|
869
|
-
}
|
|
870
|
-
catch (e2) {
|
|
871
|
-
if (!(e2 instanceof Error && e2.code === "invalid_state"))
|
|
872
|
-
throw e2;
|
|
873
|
-
}
|
|
874
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
875
|
-
}
|
|
876
|
-
throw haltRefused("the task is no longer running");
|
|
877
|
-
},
|
|
878
|
-
destroy: () => (destroyOnce ??= (async () => {
|
|
879
|
-
const h = handle ?? (await orTimeout(ready));
|
|
880
|
-
if (h) {
|
|
881
|
-
h.abortController.abort();
|
|
882
|
-
void h.harness.abort();
|
|
883
|
-
}
|
|
884
|
-
await orTimeout(run.catch(() => { }));
|
|
885
|
-
await reapSuspended();
|
|
886
|
-
})()),
|
|
403
|
+
steer,
|
|
404
|
+
notify,
|
|
405
|
+
optOutMemoryCapture,
|
|
406
|
+
compact,
|
|
407
|
+
detach,
|
|
408
|
+
interrupt,
|
|
409
|
+
halt,
|
|
410
|
+
destroy,
|
|
887
411
|
};
|
|
888
412
|
}
|
|
889
413
|
async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, captureOptOutRef, entryActor, entryTracer) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/393 S4 — the TaskStream façade's HALT verbs (T7), verbatim from `Runner.runTaskStream`: `halt` (the bare
|
|
3
|
+
* user interrupt — the finished-task refusal, the fresh liveness test on the resolved handle, `apply` with its
|
|
4
|
+
* pre-abort ownership snapshot, the conjunctive attribution seat, the unconsumed-halt and turn-cut notices, and
|
|
5
|
+
* the birth-window poll; never on the steer FIFO chain) and `destroy` (the once-memoized reap: abort, await the
|
|
6
|
+
* run's settle bounded, then the reap lane's step). Two Runner reads, both the deployment's notice sink.
|
|
7
|
+
*
|
|
8
|
+
* Both closures are the ones the one-file façade returned, installed by the driver under their old names — no
|
|
9
|
+
* await inside either moved off its tick. `destroyOnce` (the verb's memo) moved with the verb: it is written and
|
|
10
|
+
* read by `destroy` alone.
|
|
11
|
+
*/
|
|
12
|
+
import { type TaskSpec, type TaskStream } from "../types.js";
|
|
13
|
+
import type { LiveHandle, RunnerDepsSeat, TaskStreamLiveSeat } from "./contracts.js";
|
|
14
|
+
export interface StreamHaltVerbsInput {
|
|
15
|
+
/** borrowed-readonly — the task spec (`taskId` on the notices' detail). */
|
|
16
|
+
spec: TaskSpec;
|
|
17
|
+
/** borrowed-readonly — the stream's live state: `resultValue` (the finished-task refusal, the liveness tests) and `handle`. */
|
|
18
|
+
live: TaskStreamLiveSeat;
|
|
19
|
+
/** borrowed-readonly — the readiness promise; halt and destroy resolve the handle through it while the run body has not published one. */
|
|
20
|
+
ready: Promise<LiveHandle | undefined>;
|
|
21
|
+
/** borrowed-readonly — the driver's timeout race, used for the readiness wait and for destroy's wait on the run's settle. */
|
|
22
|
+
orTimeout: <T>(p: Promise<T>) => Promise<T | undefined>;
|
|
23
|
+
/** borrowed-readonly — the readiness bound in ms; the halt's birth-window poll reads it. */
|
|
24
|
+
readyTimeoutMs: number;
|
|
25
|
+
/** borrowed-readonly — the run promise: `destroy` awaits its settle (bounded) before reaping. */
|
|
26
|
+
run: Promise<void>;
|
|
27
|
+
/** borrowed-readonly — the reap lane's step, run after the run settled (the suspended-run env + checkpoint reap). */
|
|
28
|
+
reapSuspended: () => Promise<void>;
|
|
29
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`onNotice`, after the readiness await). */
|
|
30
|
+
runner: RunnerDepsSeat;
|
|
31
|
+
}
|
|
32
|
+
export interface StreamHaltVerbsResult {
|
|
33
|
+
/** The stream's `halt` verb. */
|
|
34
|
+
halt: TaskStream["halt"];
|
|
35
|
+
/** The stream's `destroy` verb (idempotent at the promise level; never throws). */
|
|
36
|
+
destroy: TaskStream["destroy"];
|
|
37
|
+
}
|
|
38
|
+
export declare function streamHaltVerbs(input: StreamHaltVerbsInput): StreamHaltVerbsResult;
|