From a3ce15acd449bb908aa8294ce2672bc5ccf38c0d Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Fri, 17 Jul 2026 15:59:45 -0600 Subject: [PATCH 1/2] feat(interface): define activation outcomes --- .changeset/clean-plums-activate.md | 5 + .../agent-candidate-promotion-schema.test.ts | 95 +++++++++++- .../src/agent-candidate-promotion-schema.ts | 140 +++++++++++++++--- .../agent-interface/src/agent-candidate.ts | 56 +++++++ 4 files changed, 277 insertions(+), 19 deletions(-) create mode 100644 .changeset/clean-plums-activate.md diff --git a/.changeset/clean-plums-activate.md b/.changeset/clean-plums-activate.md new file mode 100644 index 0000000..409e142 --- /dev/null +++ b/.changeset/clean-plums-activate.md @@ -0,0 +1,5 @@ +--- +"@tangle-network/agent-interface": minor +--- + +Add expiring apply/restore authorizations and typed idempotent activation outcomes. diff --git a/packages/agent-interface/src/agent-candidate-promotion-schema.test.ts b/packages/agent-interface/src/agent-candidate-promotion-schema.test.ts index e99e1db..c723faf 100644 --- a/packages/agent-interface/src/agent-candidate-promotion-schema.test.ts +++ b/packages/agent-interface/src/agent-candidate-promotion-schema.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; import { canonicalCandidateDigest } from "./agent-candidate-schema-common.js"; -import { agentImprovementActivationSchema } from "./agent-candidate-promotion-schema.js"; +import { + agentImprovementActivationResultSchema, + agentImprovementActivationSchema, +} from "./agent-candidate-promotion-schema.js"; const sha = (digit: string) => `sha256:${digit.repeat(64)}` as const; @@ -11,6 +14,7 @@ function activation() { reviewDigest: sha("2"), experimentDigest: sha("3"), candidateBundleDigest: sha("4"), + intent: "activate-candidate" as const, targets: [ { surface: "prompt" as const, @@ -21,6 +25,32 @@ function activation() { fundingOwner: "tenant:test", authorizedBy: "policy:tenant-admin", authorizedAt: "2026-07-15T00:00:00.000Z", + expiresAt: "2026-07-15T00:05:00.000Z", + }; + return { ...material, digest: canonicalCandidateDigest(material) }; +} + +function activationResult( + outcome: Record = { + status: "applied", + transactionId: "profile-transaction:123", + targets: [ + { + surface: "prompt", + identity: "agent-profile:support", + beforeDigest: sha("5"), + afterDigest: sha("6"), + }, + ], + }, +) { + const authority = activation(); + const material = { + kind: "agent-improvement-activation-result" as const, + idempotencyKey: authority.digest, + attemptedAt: "2026-07-15T00:01:00.000Z", + completedAt: "2026-07-15T00:01:01.000Z", + outcome, }; return { ...material, digest: canonicalCandidateDigest(material) }; } @@ -40,6 +70,16 @@ describe("agentImprovementActivationSchema", () => { ).toThrow(/unique by surface and identity/); }); + it("rejects authority that expires before it can be used", () => { + const input = activation(); + expect(() => + agentImprovementActivationSchema.parse({ + ...input, + expiresAt: input.authorizedAt, + }), + ).toThrow(/expiry must follow/); + }); + it("rejects values outside canonical JSON", () => { expect(() => agentImprovementActivationSchema.parse({ @@ -54,3 +94,56 @@ describe("agentImprovementActivationSchema", () => { ).toThrow(); }); }); + +describe("agentImprovementActivationResultSchema", () => { + it("accepts an applied transaction-wide result", () => { + expect(agentImprovementActivationResultSchema.parse(activationResult())).toEqual( + activationResult(), + ); + }); + + it.each([ + { + status: "already-applied", + targets: [ + { surface: "prompt", identity: "agent-profile:support", currentDigest: sha("6") }, + ], + }, + { + status: "conflict", + targets: [ + { surface: "prompt", identity: "agent-profile:support", currentDigest: sha("9") }, + ], + }, + { status: "expired" }, + { status: "unsupported", code: "TARGET_UNSUPPORTED", message: "No target adapter." }, + { status: "failed", code: "WRITE_REJECTED", message: "No state changed." }, + { status: "indeterminate", code: "CONNECTION_LOST", message: "Commit state is unknown." }, + ])("accepts the $status outcome", (outcome) => { + const input = activationResult(outcome); + expect(agentImprovementActivationResultSchema.parse(input)).toEqual(input); + }); + + it("rejects duplicate outcome targets", () => { + const target = { + surface: "prompt", + identity: "agent-profile:support", + currentDigest: sha("6"), + }; + const input = activationResult({ status: "already-applied", targets: [target, target] }); + expect(() => agentImprovementActivationResultSchema.parse(input)).toThrow( + /unique by surface and identity/, + ); + }); + + it("rejects a completion before its attempt", () => { + const input = activationResult(); + expect(() => + agentImprovementActivationResultSchema.parse({ + ...input, + completedAt: "2026-07-15T00:00:59.999Z", + }), + ).toThrow(/cannot predate/); + }); + +}); diff --git a/packages/agent-interface/src/agent-candidate-promotion-schema.ts b/packages/agent-interface/src/agent-candidate-promotion-schema.ts index f733166..76c2c46 100644 --- a/packages/agent-interface/src/agent-candidate-promotion-schema.ts +++ b/packages/agent-interface/src/agent-candidate-promotion-schema.ts @@ -4,6 +4,7 @@ import type { AgentCandidateJsonValue, AgentImprovementMeasuredComparison, AgentImprovementActivation, + AgentImprovementActivationResult, AgentImprovementProposal, AgentImprovementReview, CandidateExecutionEvidence, @@ -874,6 +875,14 @@ export const agentImprovementReviewSchema = z .strict() .refine(isCanonicalJsonValue, "review must contain only RFC 8785 JSON values") satisfies z.ZodType; +const improvementActivationTargetSchema = z + .object({ + surface: improvementSurfaceSchema, + identity: z.string().min(1).max(500), + expectedBaseDigest: sha256DigestSchema, + }) + .strict(); + export const agentImprovementActivationSchema = z .object({ kind: z.literal("agent-improvement-activation"), @@ -881,28 +890,14 @@ export const agentImprovementActivationSchema = z reviewDigest: sha256DigestSchema, experimentDigest: sha256DigestSchema, candidateBundleDigest: sha256DigestSchema, + intent: z.enum(["activate-candidate", "restore-baseline"]), targets: z - .tuple([ - z - .object({ - surface: improvementSurfaceSchema, - identity: z.string().min(1).max(500), - expectedBaseDigest: sha256DigestSchema, - }) - .strict(), - ]) - .rest( - z - .object({ - surface: improvementSurfaceSchema, - identity: z.string().min(1).max(500), - expectedBaseDigest: sha256DigestSchema, - }) - .strict(), - ), + .tuple([improvementActivationTargetSchema]) + .rest(improvementActivationTargetSchema), fundingOwner: z.string().min(1).max(500), authorizedBy: z.string().min(1).max(500), authorizedAt: z.iso.datetime(), + expiresAt: z.iso.datetime(), digest: sha256DigestSchema, }) .strict() @@ -917,7 +912,116 @@ export const agentImprovementActivationSchema = z message: "activation targets must be unique by surface and identity", }); } + if (Date.parse(activation.expiresAt) <= Date.parse(activation.authorizedAt)) { + ctx.addIssue({ + code: "custom", + path: ["expiresAt"], + message: "activation expiry must follow its authorization time", + }); + } if (!isCanonicalJsonValue(activation)) { ctx.addIssue({ code: "custom", message: "activation must contain only RFC 8785 JSON values" }); } }) satisfies z.ZodType; + +const improvementActivationTargetTransitionSchema = z + .object({ + surface: improvementSurfaceSchema, + identity: z.string().min(1).max(500), + beforeDigest: sha256DigestSchema, + afterDigest: sha256DigestSchema, + }) + .strict(); + +const improvementActivationTargetStateSchema = z + .object({ + surface: improvementSurfaceSchema, + identity: z.string().min(1).max(500), + currentDigest: sha256DigestSchema, + }) + .strict(); + +const improvementActivationOutcomeSchema = z.discriminatedUnion("status", [ + z + .object({ + status: z.literal("applied"), + transactionId: z.string().min(1).max(500), + targets: z + .tuple([improvementActivationTargetTransitionSchema]) + .rest(improvementActivationTargetTransitionSchema), + }) + .strict(), + z + .object({ + status: z.literal("already-applied"), + targets: z + .tuple([improvementActivationTargetStateSchema]) + .rest(improvementActivationTargetStateSchema), + }) + .strict(), + z + .object({ + status: z.literal("conflict"), + targets: z + .tuple([improvementActivationTargetStateSchema]) + .rest(improvementActivationTargetStateSchema), + }) + .strict(), + z.object({ status: z.literal("expired") }).strict(), + z + .object({ + status: z.literal("unsupported"), + code: z.string().min(1).max(100), + message: z.string().min(1).max(2_000), + }) + .strict(), + z + .object({ + status: z.literal("failed"), + code: z.string().min(1).max(100), + message: z.string().min(1).max(2_000), + }) + .strict(), + z + .object({ + status: z.literal("indeterminate"), + code: z.string().min(1).max(100), + message: z.string().min(1).max(2_000), + }) + .strict(), +]); + +export const agentImprovementActivationResultSchema = z + .object({ + kind: z.literal("agent-improvement-activation-result"), + idempotencyKey: sha256DigestSchema, + attemptedAt: z.iso.datetime(), + completedAt: z.iso.datetime(), + outcome: improvementActivationOutcomeSchema, + digest: sha256DigestSchema, + }) + .strict() + .superRefine((result, ctx) => { + if (Date.parse(result.completedAt) < Date.parse(result.attemptedAt)) { + ctx.addIssue({ + code: "custom", + path: ["completedAt"], + message: "activation completion cannot predate its attempt", + }); + } + const targets = "targets" in result.outcome ? result.outcome.targets : []; + const identities = targets.map((target) => `${target.surface}\u0000${target.identity}`); + if (new Set(identities).size !== identities.length) { + ctx.addIssue({ + code: "custom", + path: ["outcome", "targets"], + message: "activation outcome targets must be unique by surface and identity", + }); + } + if (!isCanonicalJsonValue(result)) { + ctx.addIssue({ + code: "custom", + message: "activation result must contain only RFC 8785 JSON values", + }); + } + }) satisfies z.ZodType; diff --git a/packages/agent-interface/src/agent-candidate.ts b/packages/agent-interface/src/agent-candidate.ts index 0c78fc8..6aec1c3 100644 --- a/packages/agent-interface/src/agent-candidate.ts +++ b/packages/agent-interface/src/agent-candidate.ts @@ -944,6 +944,10 @@ export interface AgentImprovementActivationTarget { expectedBaseDigest: Sha256Digest; } +export type AgentImprovementActivationIntent = + | "activate-candidate" + | "restore-baseline"; + /** Authority receipt permitting activation of one already-measured candidate. */ export interface AgentImprovementActivation { kind: "agent-improvement-activation"; @@ -951,10 +955,62 @@ export interface AgentImprovementActivation { reviewDigest: Sha256Digest; experimentDigest: Sha256Digest; candidateBundleDigest: Sha256Digest; + intent: AgentImprovementActivationIntent; targets: [AgentImprovementActivationTarget, ...AgentImprovementActivationTarget[]]; fundingOwner: string; authorizedBy: string; authorizedAt: string; + expiresAt: string; + digest: Sha256Digest; +} + +export interface AgentImprovementActivationTargetTransition { + surface: AgentImprovementSurface; + identity: string; + beforeDigest: Sha256Digest; + afterDigest: Sha256Digest; +} + +export interface AgentImprovementActivationTargetState { + surface: AgentImprovementSurface; + identity: string; + currentDigest: Sha256Digest; +} + +export type AgentImprovementActivationOutcome = + | { + /** Every target changed atomically from the authorized state to the requested bundle. */ + status: "applied"; + transactionId: string; + targets: [ + AgentImprovementActivationTargetTransition, + ...AgentImprovementActivationTargetTransition[], + ]; + } + | { + /** No write occurred; targets are already desired or no longer match the authorized base. */ + status: "already-applied" | "conflict"; + targets: [ + AgentImprovementActivationTargetState, + ...AgentImprovementActivationTargetState[], + ]; + } + | { status: "expired" } + | { + /** `failed` proves no write; `indeterminate` means commit state must be reconciled. */ + status: "unsupported" | "failed" | "indeterminate"; + code: string; + message: string; + }; + +/** Immutable outcome of one idempotent, transaction-wide activation attempt. */ +export interface AgentImprovementActivationResult { + kind: "agent-improvement-activation-result"; + /** The activation digest is also its stable retry key. */ + idempotencyKey: Sha256Digest; + attemptedAt: string; + completedAt: string; + outcome: AgentImprovementActivationOutcome; digest: Sha256Digest; } From 55bc6b4a3e4e7300060af3e3e6d62a82198445bf Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Fri, 17 Jul 2026 16:17:05 -0600 Subject: [PATCH 2/2] test(interface): cover activation variants --- .../agent-candidate-promotion-schema.test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/agent-interface/src/agent-candidate-promotion-schema.test.ts b/packages/agent-interface/src/agent-candidate-promotion-schema.test.ts index c723faf..dfc99dd 100644 --- a/packages/agent-interface/src/agent-candidate-promotion-schema.test.ts +++ b/packages/agent-interface/src/agent-candidate-promotion-schema.test.ts @@ -60,6 +60,13 @@ describe("agentImprovementActivationSchema", () => { expect(agentImprovementActivationSchema.parse(activation())).toEqual(activation()); }); + it("accepts restore authority", () => { + const { digest: _digest, ...authority } = activation(); + const material = { ...authority, intent: "restore-baseline" as const }; + const restore = { ...material, digest: canonicalCandidateDigest(material) }; + expect(agentImprovementActivationSchema.parse(restore)).toEqual(restore); + }); + it("rejects duplicate surface identities", () => { const input = activation(); expect(() => @@ -136,6 +143,23 @@ describe("agentImprovementActivationResultSchema", () => { ); }); + it("rejects duplicate applied targets", () => { + const target = { + surface: "prompt", + identity: "agent-profile:support", + beforeDigest: sha("5"), + afterDigest: sha("6"), + }; + const input = activationResult({ + status: "applied", + transactionId: "profile-transaction:123", + targets: [target, target], + }); + expect(() => agentImprovementActivationResultSchema.parse(input)).toThrow( + /unique by surface and identity/, + ); + }); + it("rejects a completion before its attempt", () => { const input = activationResult(); expect(() => @@ -145,5 +169,4 @@ describe("agentImprovementActivationResultSchema", () => { }), ).toThrow(/cannot predate/); }); - });