Status: IMPLEMENTATION_READY
Date: 2026-04-13
Package: @agent-assistant/continuation
Version target: v0.1.0 (pre-1.0, provisional)
Adoption posture: direct-import / wave-2 until implementation and one consumer proof exist
@agent-assistant/continuation owns the bounded runtime lifecycle for a stopped-but-resumable assistant turn.
It exists to convert resumable harness outcomes into:
- explicit persisted continuation state
- validated resume triggers
- resumed bounded turns
- inspectable follow-up delivery state
Owns:
- continuation record creation and lifecycle
- typed wait conditions and resume triggers
- TTL / expiry / resume-attempt bounds
- resumed-turn re-entry contract
- follow-up delivery status contract
- continuation-specific stop reasons
- store and adapter interfaces needed for the above
Does NOT own:
- bounded turn execution itself (→
@agent-assistant/harness) - turn-scoped identity/context assembly (→
@agent-assistant/turn-context) - session identity/lifecycle (→
@agent-assistant/sessions) - transport delivery implementations (→
@agent-assistant/surfaces) - approval/risk decisions (→
@agent-assistant/policy) - long-term memory persistence (→
@agent-assistant/memory) - generic reminders/watch rules (→
@agent-assistant/proactive) - product business heuristics
- not a background autonomous agent framework
- not a generic workflow engine
- not a scheduler abstraction for arbitrary jobs
- not a memory system
- not a policy engine
- not a multi-branch continuation graph system in v1
- not an invisible retry loop that keeps acting after returning
A continuation lifecycle begins only after a bounded turn returns a resumable harness result.
Canonical shape:
- product runs one bounded turn through harness
- harness returns one of:
needs_clarificationawaiting_approvaldeferred
- product/runtime creates a
ContinuationRecord - runtime waits for one explicit resume trigger
- continuation validates the trigger and record liveness
- continuation creates a new bounded resumed turn invocation
- harness runs again and returns a new
HarnessResult - continuation either:
- terminates the record, or
- replaces/updates it with a fresh pending state if still resumable
- user-visible follow-up is delivered or truthfully suppressed
export interface ContinuationRuntime {
create(input: CreateContinuationInput): Promise<ContinuationCreateResult>;
resume(input: ResumeContinuationInput): Promise<ContinuationResumeResult>;
stop(input: StopContinuationInput): Promise<ContinuationStopResult>;
get(input: { continuationId: string }): Promise<ContinuationRecord | null>;
}export interface ContinuationConfig {
store: ContinuationStore;
harness: ContinuationHarnessAdapter;
delivery?: ContinuationDeliveryAdapter;
scheduler?: ContinuationSchedulerAdapter;
clock?: ContinuationClock;
trace?: ContinuationTraceSink;
defaults?: ContinuationDefaults;
}export interface ContinuationDefaults {
clarificationTtlMs?: number;
approvalTtlMs?: number;
deferredTtlMs?: number;
scheduledWakeTtlMs?: number;
maxResumeAttempts?: number;
}export interface CreateContinuationInput {
assistantId: string;
sessionId?: string;
threadId?: string;
userId?: string;
originTurnId: string;
harnessResult: HarnessResult;
delivery?: ContinuationDeliveryTarget;
bounds?: Partial<ContinuationBounds>;
metadata?: Record<string, unknown>;
}create() must reject non-resumable harness results.
Only these outcomes may produce a live continuation in v1:
needs_clarificationawaiting_approvaldeferred
completed and failed are terminal and must not create a live continuation.
export interface ResumeContinuationInput {
continuationId: string;
trigger: ContinuationResumeTrigger;
metadata?: Record<string, unknown>;
}export interface StopContinuationInput {
continuationId: string;
reason: ContinuationTerminalReason;
metadata?: Record<string, unknown>;
}export interface ContinuationRecord {
id: string;
assistantId: string;
sessionId?: string;
threadId?: string;
userId?: string;
origin: ContinuationOrigin;
status: ContinuationStatus;
waitFor: ContinuationWaitCondition;
continuation: HarnessContinuation;
delivery: ContinuationDeliveryState;
bounds: ContinuationBounds;
createdAt: string;
updatedAt: string;
lastResumedAt?: string;
terminalReason?: ContinuationTerminalReason;
metadata?: Record<string, unknown>;
}export interface ContinuationOrigin {
turnId: string;
outcome: 'needs_clarification' | 'awaiting_approval' | 'deferred';
stopReason: string;
createdAt: string;
}export type ContinuationStatus =
| 'pending'
| 'resuming'
| 'completed'
| 'cancelled'
| 'expired'
| 'superseded'
| 'failed';export type ContinuationWaitCondition =
| { type: 'user_reply'; correlationKey?: string }
| { type: 'approval_resolution'; approvalId: string }
| { type: 'external_result'; operationId: string }
| { type: 'scheduled_wake'; wakeUpId?: string };export interface ContinuationBounds {
expiresAt: string;
maxResumeAttempts: number;
resumeAttempts: number;
}export type ContinuationTerminalReason =
| 'completed'
| 'cancelled_by_user'
| 'cancelled_by_product'
| 'expired_ttl'
| 'superseded_by_newer_turn'
| 'approval_denied'
| 'invalid_resume_trigger'
| 'max_resume_attempts_reached'
| 'resume_runtime_error'
| 'delivery_failed'
| 'session_no_longer_deliverable';export type ContinuationResumeTrigger =
| {
type: 'user_reply';
message: HarnessUserMessage;
receivedAt: string;
}
| {
type: 'approval_resolution';
approvalId: string;
decision: 'approved' | 'denied';
resolvedAt: string;
metadata?: Record<string, unknown>;
}
| {
type: 'external_result';
operationId: string;
resolvedAt: string;
payload?: Record<string, unknown>;
}
| {
type: 'scheduled_wake';
wakeUpId?: string;
firedAt: string;
};export interface ContinuationDeliveryTarget {
surfaceIds?: string[];
fanoutMode?: 'originating_surface' | 'attached_surfaces' | 'product_defined';
suppressIfSessionReengaged?: boolean;
}export interface ContinuationDeliveryState {
target?: ContinuationDeliveryTarget;
status:
| 'not_applicable'
| 'pending_delivery'
| 'delivered'
| 'suppressed_session_reengaged'
| 'suppressed_superseded'
| 'suppressed_expired'
| 'delivery_failed';
lastDeliveryAttemptAt?: string;
deliveredAt?: string;
}export interface ContinuationStore {
put(record: ContinuationRecord): Promise<void>;
get(continuationId: string): Promise<ContinuationRecord | null>;
delete?(continuationId: string): Promise<void>;
listBySession?(sessionId: string): Promise<ContinuationRecord[]>;
}V1 only requires point reads/writes. Advanced querying is optional.
export interface ContinuationHarnessAdapter {
runResumedTurn(input: ContinuationResumedTurnInput): Promise<HarnessResult>;
}export interface ContinuationResumedTurnInput {
continuation: ContinuationRecord;
trigger: ContinuationResumeTrigger;
resumedTurnId: string;
}This adapter exists so continuation does not need to know how product code assembles turn-context before calling harness.
export interface ContinuationDeliveryAdapter {
deliver(input: ContinuationDeliveryInput): Promise<ContinuationDeliveryResult>;
}export interface ContinuationDeliveryInput {
continuation: ContinuationRecord;
harnessResult: HarnessResult;
}export interface ContinuationDeliveryResult {
status:
| 'delivered'
| 'suppressed_session_reengaged'
| 'suppressed_superseded'
| 'suppressed_expired'
| 'delivery_failed';
deliveredAt?: string;
metadata?: Record<string, unknown>;
}export interface ContinuationSchedulerAdapter {
requestWakeUp(at: Date, context: { continuationId: string }): Promise<string>;
cancelWakeUp?(wakeUpId: string): Promise<void>;
}This adapter is optional and only for the v1 scheduled_wake case tied to a live continuation.
create() must map harness outcomes to wait conditions as follows.
- status →
pending - waitFor →
{ type: 'user_reply' } - TTL default →
clarificationTtlMs
- status →
pending - waitFor →
{ type: 'approval_resolution', approvalId } - TTL default →
approvalTtlMs - if no approval correlation id exists in the harness continuation payload, creation must fail as invalid input
- status →
pending - waitFor must be derivable from the continuation payload or product-supplied metadata
- valid v1 deferred wait conditions:
external_resultscheduled_wake
- TTL default →
deferredTtlMsunlessscheduled_wakethenscheduledWakeTtlMs
When bounds are omitted:
resumeAttemptsstarts at0maxResumeAttemptsdefaults from configexpiresAtderives from outcome-specific TTL default
If the product supplies bounds, the package should validate:
expiresAtis in the futuremaxResumeAttempts >= 1resumeAttempts >= 0
resume() must validate, in order:
- record exists
- record status is
pending - current time is before
expiresAt - trigger type matches
waitFor.type - correlation identifiers match where applicable
resumeAttempts < maxResumeAttempts
If any check fails, the runtime must return a terminal or no-op result with a truthful reason.
If the trigger is approval_resolution and decision === 'denied':
- do not re-enter harness
- mark record terminal with
approval_denied - delivery may be product-defined; v1 may treat this as terminal-without-follow-up unless the delivery adapter chooses otherwise
Before calling harness:
- increment
resumeAttempts - update status to
resuming - persist the updated record
After harness returns:
- if result is terminal (
completedorfailed) → stop record accordingly - if result is resumable again → replace/update record with fresh pending state and new origin turn id
Every resume must create a new bounded turn id.
The original turn id remains in origin.turnId for lineage.
When a resumed turn yields a user-visible terminal result, the runtime should attempt delivery through ContinuationDeliveryAdapter if configured.
The delivery adapter may internally call product/runtime emit methods backed by @agent-assistant/surfaces, but this package does not define transport delivery protocols itself.
The delivery adapter may truthfully suppress a follow-up when:
- session re-engagement made it stale
- continuation was superseded before delivery
- continuation expired before delivery
The store must persist final delivery status on the record before completion is reported.
export interface ContinuationCreateResult {
record: ContinuationRecord;
}export interface ContinuationResumeResult {
record: ContinuationRecord;
harnessResult?: HarnessResult;
delivery?: ContinuationDeliveryResult;
}export interface ContinuationStopResult {
record: ContinuationRecord;
}V1 should expose trace hooks for:
- continuation created
- continuation creation rejected
- continuation resume requested
- continuation resume rejected
- continuation expired
- continuation resumed turn started
- continuation resumed turn finished
- continuation delivery attempted
- continuation delivery finalized
- continuation terminated
Minimum trace fields:
continuationIdassistantIdsessionId?originTurnIdresumedTurnId?statuswaitFor.typeterminalReason?- timestamp
packages/continuation/
package.json
tsconfig.json
README.md
src/
index.ts
types.ts
continuation.ts
continuation.test.ts
The first implementation should stay compact and adapter-driven.
A truthful v1 is ready only if all of the following are true.
- canonical boundary and spec exist
- README explains create/resume/stop semantics
- harness / proactive / sessions boundaries are explicit
- non-resumable harness results are rejected at create time
- resumable results create valid continuation records
- resume preflight validation is enforced
- expiry and max-attempt rules are enforced
- approval-denied path is terminal without false resume
- resumed work always re-enters harness as a new bounded turn
- delivery state is persisted truthfully
- clarification continuation path
- approval continuation path
- approval denied path
- external-result deferred path
- scheduled-wake deferred path
- invalid trigger rejection
- TTL expiry path
- max-resume-attempt path
- superseded path
- delivery suppressed path
- delivery failed path
- resumed result creates a fresh pending continuation path
- at least one realistic consumer proof exists where:
- harness produces a resumable outcome
- continuation persists it
- a later trigger resumes it
- follow-up is delivered or truthfully suppressed
@agent-assistant/continuation is implementation-ready when kept inside this sentence:
It is the bounded runtime that owns resumable turn state and follow-up delivery after a harness result stops honestly but not finally.
If implementation drifts into general workflow orchestration, scheduler ownership, or autonomy loops, the boundary has been violated.
V1_CONTINUATION_SPEC_READY