Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/exact-retained-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tangle-network/agent-interface": minor
---

Define explicit retained-run capability proof and digest-bound, retry-safe cancellation requests and acknowledgements.
5 changes: 4 additions & 1 deletion packages/agent-interface/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ shapes; higher-level packages import from here rather than redefining them.

## Durable runs, interactions, and context

`AgentRunControlRef` identifies a retained run without depending on a live JavaScript object.
`AgentRunControlRef` identifies a retained run without depending on a live JavaScript object and may carry the provider's admission digest so reconstruction can reject changed-input reuse.
`RuntimeEventEnvelope` adds stable run, event, sequence, cursor, and timestamp fields around the existing `StreamEvent` union, and its runtime schema validates every canonical event variant.
Providers advertise `retainedControl` only when exact run, result, event, cancellation, replay, detach, turn, and session identity are all implemented together.
`AgentSession.cancelRun()` accepts a canonical request digest bound to one operation and `AgentRunControlRef`, so a caller can safely repeat the same cancellation after losing the first acknowledgement.
Its acknowledgement repeats the operation, digest, and run coordinates and distinguishes a known cancellation effect from conflict or unknown state.

An environment advertises `interactions` only when it can originate and answer typed requests.
`AgentEnvironmentCapabilitiesSchema` strictly validates the complete capability document at runtime, including all-or-nothing durable branching declarations.
Expand Down
28 changes: 28 additions & 0 deletions packages/agent-interface/src/environment-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,34 @@ describe("AgentEnvironmentCapabilitiesSchema", () => {
).toThrow(/requires session continuation/);
});

it("advertises retained run control only with every identity guarantee", () => {
const retainedControl = {
exactRunIdentity: true,
resultIdentity: true,
eventIdentity: true,
cancellationIdempotency: true,
};
expect(
AgentEnvironmentCapabilitiesSchema.parse({
...capabilities,
retainedControl,
}),
).toMatchObject({ retainedControl });
expect(() =>
AgentEnvironmentCapabilitiesSchema.parse({
...capabilities,
retainedControl: { ...retainedControl, resultIdentity: false },
}),
).toThrow(/retained control requires exact run/);
expect(() =>
AgentEnvironmentCapabilitiesSchema.parse({
...capabilities,
streaming: { ...capabilities.streaming, detach: false },
retainedControl,
}),
).toThrow(/retained control requires exact run/);
});

it("rejects duplicate open capability values", () => {
expect(() =>
AgentEnvironmentCapabilitiesSchema.parse({
Expand Down
40 changes: 40 additions & 0 deletions packages/agent-interface/src/environment-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import {
type NativeContextContinuationTurn,
} from "./portable-context.js";
import {
type AgentRunCancellationAcknowledgement,
type AgentRunCancellationRequest,
AgentRunControlRefSchema,
CanonicalStreamEventSchema,
type AgentRunControlRef,
Expand Down Expand Up @@ -489,6 +491,11 @@ export interface AgentSession {
request: NativeContextContinuationRequest,
options: AgentNativeContextContinuationOptions,
): Promise<AgentNativeContextContinuationResult>;
/** Retry-safe cancellation bound to one exact run and caller operation. */
cancelRun?(
request: AgentRunCancellationRequest,
options?: { signal?: AbortSignal },
): Promise<AgentRunCancellationAcknowledgement>;
cancel(): Promise<void>;
}

Expand Down Expand Up @@ -539,6 +546,13 @@ export interface AgentEnvironmentCapabilities {
list: boolean;
messages: boolean;
};
/** Present only when every retained-run identity and retry promise is implemented. */
retainedControl?: {
exactRunIdentity: boolean;
resultIdentity: boolean;
eventIdentity: boolean;
cancellationIdempotency: boolean;
};
/** Absent when verified, retry-safe same-session continuation is unsupported. */
nativeContinuation?: {
/** Boundary comparison and operation admission are one atomic provider action. */
Expand Down Expand Up @@ -590,6 +604,14 @@ export const AgentEnvironmentCapabilitiesSchema = z
list: z.boolean(),
messages: z.boolean(),
}),
retainedControl: z
.strictObject({
exactRunIdentity: z.boolean(),
resultIdentity: z.boolean(),
eventIdentity: z.boolean(),
cancellationIdempotency: z.boolean(),
})
.optional(),
nativeContinuation: z
.strictObject({
atomicBoundary: z.boolean(),
Expand Down Expand Up @@ -622,6 +644,24 @@ export const AgentEnvironmentCapabilitiesSchema = z
.optional(),
})
.superRefine((capabilities, refinement) => {
if (
capabilities.retainedControl !== undefined &&
(!capabilities.retainedControl.exactRunIdentity ||
!capabilities.retainedControl.resultIdentity ||
!capabilities.retainedControl.eventIdentity ||
!capabilities.retainedControl.cancellationIdempotency ||
!capabilities.streaming.replay ||
!capabilities.streaming.detach ||
!capabilities.streaming.turnIdempotency ||
!capabilities.sessions.continue)
) {
refinement.addIssue({
code: "custom",
path: ["retainedControl"],
message:
"retained control requires exact run, result, event, cancellation, replay, detach, turn, and session identity together",
});
}
if (
capabilities.nativeContinuation !== undefined &&
(!capabilities.nativeContinuation.atomicBoundary ||
Expand Down
43 changes: 43 additions & 0 deletions packages/agent-interface/src/runtime-control.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { describe, expect, it } from "vitest";
import {
AgentRunCancellationAcknowledgementSchema,
AgentRunCancellationRequestSchema,
AgentRunControlRefSchema,
CanonicalStreamEventSchema,
RuntimeEventEnvelopeSchema,
agentRunCancellationAcknowledgementMatchesRequest,
agentRunCancellationRequestDigest,
} from "./runtime-control.js";

describe("durable run control", () => {
Expand All @@ -13,12 +17,51 @@ describe("durable run control", () => {
environmentId: "local-1",
sessionId: "session-1",
executionId: "execution-1",
requestDigest: `sha256:${"a".repeat(64)}`,
};
expect(AgentRunControlRefSchema.parse(reference)).toEqual(reference);
expect(() =>
AgentRunControlRefSchema.parse({ ...reference, provider: " cli-bridge" }),
).toThrow(/outer whitespace/);
});

it("binds a retry-safe cancellation to one exact run and request digest", () => {
const material = {
operationId: "cancel-1",
run: {
runId: "run-1",
provider: "cli-bridge",
environmentId: "local-1",
sessionId: "session-1",
executionId: "execution-1",
},
reason: "user requested stop",
};
const request = AgentRunCancellationRequestSchema.parse({
...material,
requestDigest: agentRunCancellationRequestDigest(material),
});
const acknowledgement = AgentRunCancellationAcknowledgementSchema.parse({
operationId: request.operationId,
requestDigest: request.requestDigest,
run: request.run,
status: "accepted",
effect: "cancel_requested",
});
expect(
agentRunCancellationAcknowledgementMatchesRequest(request, acknowledgement),
).toBe(true);
expect(() =>
AgentRunCancellationRequestSchema.parse({ ...request, reason: "changed" }),
).toThrow(/digest/);
expect(() =>
AgentRunCancellationAcknowledgementSchema.parse({
...acknowledgement,
status: "accepted",
effect: "unknown",
}),
).toThrow(/certainty/);
});
});

describe("runtime event envelope", () => {
Expand Down
102 changes: 102 additions & 0 deletions packages/agent-interface/src/runtime-control.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { z } from "zod";
import type { StreamEvent } from "./index.js";
import {
canonicalCandidateDigest,
sha256DigestSchema,
} from "./agent-candidate-schema-common.js";
import type { Sha256Digest } from "./agent-candidate.js";
import { InteractionRequestSchema } from "./interaction.js";
import { DurablePlanSchema } from "./plan.js";

Expand All @@ -16,6 +21,8 @@ export interface AgentRunControlRef {
environmentId: string;
sessionId?: string;
executionId?: string;
/** Provider admission digest for detecting changed-input run reuse. */
requestDigest?: Sha256Digest;
}

export const AgentRunControlRefSchema = z.strictObject({
Expand All @@ -24,8 +31,103 @@ export const AgentRunControlRefSchema = z.strictObject({
environmentId: stableIdSchema,
sessionId: stableIdSchema.optional(),
executionId: stableIdSchema.optional(),
requestDigest: sha256DigestSchema.optional(),
}) satisfies z.ZodType<AgentRunControlRef>;

export type AgentRunCancellationEffect =
| "cancel_requested"
| "cancelled"
| "not_live"
| "unknown";

export interface AgentRunCancellationRequestMaterial {
operationId: string;
run: AgentRunControlRef;
reason?: string;
}

export interface AgentRunCancellationRequest
extends AgentRunCancellationRequestMaterial {
requestDigest: Sha256Digest;
}

export function agentRunCancellationRequestDigest(
request: AgentRunCancellationRequestMaterial,
): Sha256Digest {
return canonicalCandidateDigest({
operationId: request.operationId,
run: AgentRunControlRefSchema.parse(request.run),
...(request.reason === undefined ? {} : { reason: request.reason }),
});
}

export const AgentRunCancellationRequestSchema = z
.strictObject({
operationId: stableIdSchema,
requestDigest: sha256DigestSchema,
run: AgentRunControlRefSchema,
reason: z.string().min(1).max(2_048).optional(),
})
.superRefine((request, refinement) => {
if (request.requestDigest !== agentRunCancellationRequestDigest(request)) {
refinement.addIssue({
code: "custom",
path: ["requestDigest"],
message: "run cancellation request digest does not match its content",
});
}
}) satisfies z.ZodType<AgentRunCancellationRequest>;

export interface AgentRunCancellationAcknowledgement {
operationId: string;
requestDigest: Sha256Digest;
run: AgentRunControlRef;
status: "accepted" | "replayed" | "conflict" | "unknown";
effect: AgentRunCancellationEffect;
message?: string;
retryable?: boolean;
}

export const AgentRunCancellationAcknowledgementSchema = z
.strictObject({
operationId: stableIdSchema,
requestDigest: sha256DigestSchema,
run: AgentRunControlRefSchema,
status: z.enum(["accepted", "replayed", "conflict", "unknown"]),
effect: z.enum(["cancel_requested", "cancelled", "not_live", "unknown"]),
message: z.string().min(1).optional(),
retryable: z.boolean().optional(),
})
.superRefine((acknowledgement, refinement) => {
const known = acknowledgement.effect !== "unknown";
if (
((acknowledgement.status === "accepted" || acknowledgement.status === "replayed") && !known) ||
((acknowledgement.status === "conflict" || acknowledgement.status === "unknown") && known)
) {
refinement.addIssue({
code: "custom",
path: ["effect"],
message: "cancellation status and effect certainty do not agree",
});
}
}) satisfies z.ZodType<AgentRunCancellationAcknowledgement>;

export function agentRunCancellationAcknowledgementMatchesRequest(
request: AgentRunCancellationRequest,
acknowledgement: AgentRunCancellationAcknowledgement,
): boolean {
const exactRequest = AgentRunCancellationRequestSchema.safeParse(request);
const exactAcknowledgement =
AgentRunCancellationAcknowledgementSchema.safeParse(acknowledgement);
if (!exactRequest.success || !exactAcknowledgement.success) return false;
return (
exactAcknowledgement.data.operationId === exactRequest.data.operationId &&
exactAcknowledgement.data.requestDigest === exactRequest.data.requestDigest &&
canonicalCandidateDigest(exactAcknowledgement.data.run) ===
canonicalCandidateDigest(exactRequest.data.run)
);
}

const unknownRecordSchema = z.record(z.string(), z.unknown());
const partBase = {
id: stableIdSchema,
Expand Down
Loading