feat(agent): add pico compile-time foundation

This commit is contained in:
Mario Zechner 2026-09-12 00:02:58 +02:00
parent 4819cc877e
commit f467cf7068
10 changed files with 1507 additions and 291 deletions

View file

@ -5,24 +5,22 @@
[`pico-simple-handoff.md`](pico-simple-handoff.md) is the sole normative implementation specification.
Read it completely before implementation. Other Pico documents and prototypes are historical inputs only.
Astra reviewer `e5771f` reviewed the complete rewritten specification through multiple iterations and
returned explicit `APPROVED`. Keep the reviewer available for implementation follow-up.
The original specification and WP1 received delegated review. Peer review then identified and resolved
TaskOutput contract inconsistencies in JSONL replay, mutation failure handling, watch reconnection,
kind replacement and generation partial state.
## Git state
- Branch: `pico`
- Branch base when created: `f3c672245`
- No production implementation has started.
- The documentation preservation commit includes the pre-implementation Pico design cleanups and the
approved simple handoff.
- Branch base when created: `f3c672245`.
- WP1 implements compile-time declarations under `packages/agent/src/harness/pico/` with focused compile
coverage.
- Implement and commit one small work package at a time for user review.
## Next action
Start WP1 from `pico-simple-handoff.md`: core types and kind witnesses under
`packages/agent/src/harness/pico/`, including compile-time tests. Delegate implementation with provider
`openai-codex`, model `gpt-5.6-sol`. Run the focused test and full `npm run check`, then use retained Astra
for review. Commit only WP1 files after the package boundary is approved.
Review the WP1 commit, then begin WP2 only after explicit user approval. WP2 is the mutation algebra and
`MemoryStorage`; follow its prerequisites, acceptance criteria and one-commit boundary in the handoff.
## Adopted core
@ -35,4 +33,5 @@ abort, then a restricted abort closure.
manual collapse is background plus turn. Non-turn tasks use limited transactions and `accept`/`write`;
turn tasks may append model-affecting entries directly. Every missing live kind becomes orphaned at open.
Input groups belong to built-in generation/post_tools. V1 is job-first and excludes arbitrary unfinished
promise adoption. Tool/job output sharing remains gated.
promise adoption. Shared TaskOutput storage/runtime/watch foundations are settled; concrete tool/job
payloads, bounded capture/spill policy and model projection remain gated.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,77 @@
import type { Id, JsonValue } from "./core.ts";
export type Scope =
| { readonly type: "session" }
| { readonly type: "conversation"; readonly conversationId: Id }
| { readonly type: "task"; readonly taskId: Id }
| { readonly type: "shared"; readonly id: Id };
declare const addressType: unique symbol;
export interface Address<T extends JsonValue = JsonValue> {
readonly scope: Scope;
readonly namespace: string;
readonly key?: string;
readonly collection: "value" | "list";
readonly rewind: boolean;
readonly [addressType]?: T;
}
export interface Value<T extends JsonValue> extends Address<T> {
readonly collection: "value";
}
export interface List<T extends JsonValue> extends Address<T> {
readonly collection: "list";
}
export interface Element<T extends JsonValue> {
readonly id: Id;
readonly value: T;
}
type StickyOptions = { readonly key?: string; readonly rewind?: false };
type ConversationOptions = { readonly key?: string; readonly rewind: boolean };
type StickyScope = Extract<Scope, { type: "session" | "task" | "shared" }>;
type ConversationScope = Extract<Scope, { type: "conversation" }>;
type Options = StickyOptions | ConversationOptions;
export function defineValue<T extends JsonValue>(
scope: StickyScope,
namespace: string,
options?: StickyOptions,
): Value<T>;
export function defineValue<T extends JsonValue>(
scope: ConversationScope,
namespace: string,
options: ConversationOptions,
): Value<T>;
export function defineValue<T extends JsonValue>(scope: Scope, namespace: string, options?: Options): Value<T> {
return Object.freeze({
...options,
scope,
namespace,
collection: "value",
rewind: options?.rewind ?? false,
});
}
export function defineList<T extends JsonValue>(
scope: StickyScope,
namespace: string,
options?: StickyOptions,
): List<T>;
export function defineList<T extends JsonValue>(
scope: ConversationScope,
namespace: string,
options: ConversationOptions,
): List<T>;
export function defineList<T extends JsonValue>(scope: Scope, namespace: string, options?: Options): List<T> {
return Object.freeze({
...options,
scope,
namespace,
collection: "list",
rewind: options?.rewind ?? false,
});
}

View file

@ -0,0 +1,8 @@
export type JsonPrimitive = null | boolean | number | string;
export type JsonValue = JsonPrimitive | JsonObject | readonly JsonValue[];
export interface JsonObject {
readonly [key: string]: JsonValue;
}
export type Id = number;

View file

@ -0,0 +1,66 @@
import type { AssistantMessage, Message, ToolResultMessage, UserMessage } from "@earendil-works/pi-ai";
import type { Id, JsonValue } from "./core.ts";
export type ContextEdit =
| { readonly target: Id; readonly action: "omit" }
| { readonly target: Id; readonly action: "replace"; readonly messages: readonly Message[] };
export interface EntryIdentity {
readonly id: Id;
readonly conversationId: Id;
readonly kind: string;
readonly byTaskId?: Id;
}
export type EntryBase = EntryIdentity;
export interface EntryData<D extends JsonValue = JsonValue> {
readonly data: D;
}
export interface ModelProjection<M extends Message = Message> {
readonly model: readonly M[];
}
export interface ContextHead {
readonly head: Id;
}
export interface ContextEdits {
readonly edits: readonly ContextEdit[];
}
export type Entry = EntryBase & Partial<EntryData & ModelProjection & ContextHead & ContextEdits>;
export type EntryInput<E extends Entry> = Omit<E, keyof EntryIdentity | "head"> &
(E extends ContextHead ? { readonly head: Id | "self" } : { readonly head?: never });
export interface EntryKind<E extends Entry = Entry> {
readonly kind: string;
is(entry: Entry | undefined): entry is E;
}
export function defineEntry<E extends Entry>(kind: string): EntryKind<E> {
return Object.freeze({
kind,
is(entry: Entry | undefined): entry is E {
return entry?.kind === kind;
},
});
}
export interface Conversation {
readonly id: Id;
readonly parent?: { readonly conversationId: Id; readonly at: Id };
readonly owner?: Id;
}
export type UserEntry = EntryBase & { readonly model: readonly [UserMessage] };
export type AssistantEntry = EntryBase & { readonly model: readonly [AssistantMessage] };
export type ToolResultEntry = EntryBase &
EntryData<JsonValue> & { readonly model: readonly [ToolResultMessage<JsonValue>] };
export type NoticeEntry = EntryBase & Partial<EntryData> & { readonly model: readonly [UserMessage] };
export type SummaryEntry = EntryBase &
EntryData<{ readonly through: Id }> & { readonly model: readonly [UserMessage] } & ContextHead;
export type HandoffEntry = EntryBase & { readonly model: readonly [UserMessage] } & ContextHead;
export type ResetEntry = EntryBase & ContextHead;

View file

@ -0,0 +1,5 @@
export * from "./addresses.ts";
export * from "./core.ts";
export * from "./entries.ts";
export * from "./runtime.ts";
export * from "./tasks.ts";

View file

@ -0,0 +1,300 @@
import type { Context } from "@earendil-works/chord";
import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai";
import type { Element, List, Value } from "./addresses.ts";
import type { Id, JsonValue } from "./core.ts";
import type { ContextEdit, Conversation, Entry, EntryInput, EntryKind } from "./entries.ts";
import type {
AnyTaskKind,
Completion,
ExactJsonInput,
InputOf,
NoExtra,
OutputOf,
RunningTask,
Task,
TaskCheckpoint,
TaskOf,
TaskOutputRef,
} from "./tasks.ts";
export interface Cursor {
readonly after?: Id;
readonly before?: Id;
readonly limit: number;
}
export interface Page<T> {
readonly items: readonly T[];
readonly next?: Id;
readonly readAt: Id;
}
export interface ListQuery extends Cursor {
readonly at?: Id;
}
export interface TxReaders {
getConversation(id: Id): Promise<Conversation | undefined>;
getEntry(id: Id): Promise<Entry | undefined>;
getEntry<E extends Entry>(kind: EntryKind<E>, id: Id): Promise<E | undefined>;
getEntries(ids: readonly Id[]): Promise<ReadonlyMap<Id, Entry>>;
getTask(id: Id): Promise<Task | undefined>;
getTask<K extends AnyTaskKind>(kind: K, id: Id): Promise<TaskOf<K> | undefined>;
getTasks(ids: readonly Id[]): Promise<ReadonlyMap<Id, Task>>;
}
export interface TxValue<T extends JsonValue> {
get(at?: Id): Promise<T | undefined>;
set(value: T): void;
delete(): void;
}
export interface TxList<T extends JsonValue> {
read(query: ListQuery): Promise<Page<Element<T>>>;
append(value: T): Id;
remove(id: Id): void;
clear(): void;
}
export interface Acceptance {
readonly requestId?: string;
readonly conversationId: Id;
readonly inputId: Id;
}
interface TaskSpecBase<I extends JsonValue> {
readonly conversationId?: Id;
readonly input: I;
readonly after?: readonly Id[];
readonly background?: true;
}
export type TaskSpec<I extends JsonValue, O extends object = never> = TaskSpecBase<I> &
([O] extends [never] ? { readonly output?: never } : { readonly output?: TaskOutputRef<O> });
export interface ConversationCreateSpec {
readonly parent?: { readonly conversationId: Id; readonly at: Id };
}
export type UserInput = string | readonly (TextContent | ImageContent)[];
export interface StoredEntryDraft {
readonly kind: string;
readonly data?: JsonValue;
readonly model?: readonly Message[];
readonly head?: Id | "self";
readonly edits?: readonly ContextEdit[];
}
export type QueuedInput =
| { readonly mode: "steer" | "followUp" | "nextRun"; readonly input: UserInput; readonly requestId?: string }
| { readonly mode: "write"; readonly entry: StoredEntryDraft; readonly requestId?: string };
export type InputResult =
| { readonly status: "queued"; readonly requestId?: string }
| { readonly status: "placed"; readonly requestId?: string; readonly entry: Id }
| { readonly status: "done"; readonly requestId?: string; readonly entry: Id; readonly answer?: Id }
| {
readonly status: "unanswered";
readonly requestId?: string;
readonly entry?: Id;
readonly reason: "terminated" | "aborted" | "failed" | "stale";
readonly detail?: string;
};
export type TerminalInputResult = Extract<InputResult, { status: "done" | "unanswered" }>;
export interface AcceptOptions {
readonly input: UserInput;
readonly requestId?: string;
readonly whenBusy?: "followUp" | "steer" | "reject";
}
export interface BaseTaskTx<C extends TaskCheckpoint> extends TxReaders {
checkpoint(value: C): void;
task<K extends AnyTaskKind, S extends TaskSpec<InputOf<K>, OutputOf<K>>>(
kind: K,
spec: NoExtra<TaskSpec<InputOf<K>, OutputOf<K>>, S> & {
readonly input: ExactJsonInput<InputOf<K>, S["input"]>;
},
): Id;
createConversation(spec: ConversationCreateSpec): Id;
value<T extends JsonValue>(address: Value<T>): TxValue<T>;
list<T extends JsonValue>(address: List<T>): TxList<T>;
accept(conversationId: Id, options: AcceptOptions): Promise<Acceptance>;
queueInput(conversationId: Id, input: QueuedInput): Promise<Acceptance>;
write<E extends Entry>(
conversationId: Id,
kind: EntryKind<E>,
input: EntryInput<E>,
requestId?: string,
): Promise<Acceptance>;
}
export interface TurnTaskTx<C extends TaskCheckpoint> extends BaseTaskTx<C> {
entry<E extends Entry>(kind: EntryKind<E>, conversationId: Id, input: EntryInput<E>): Id;
}
export interface PublicValue<T extends JsonValue> {
get(at: Id | undefined, ctx: Context): Promise<T | undefined>;
set(value: T, ctx: Context): Promise<void>;
delete(ctx: Context): Promise<void>;
}
export interface PublicList<T extends JsonValue> {
read(query: ListQuery, ctx: Context): Promise<Page<Element<T>>>;
append(value: T, ctx: Context): Promise<Id>;
remove(id: Id, ctx: Context): Promise<void>;
clear(ctx: Context): Promise<void>;
}
export interface TaskOutput<O extends object> {
readonly ref: TaskOutputRef<O>;
read(ctx: Context): Promise<O>;
mutate(mutator: (state: O) => undefined, ctx: Context): Promise<void>;
replace(value: O, ctx: Context): Promise<void>;
}
export interface ReadonlyTaskOutput<O extends object> {
readonly ref: TaskOutputRef<O>;
read(ctx: Context): Promise<O>;
}
type RuntimeOutput<O extends object> = [O] extends [never] ? unknown : { readonly output: TaskOutput<O> };
type AbortRuntimeOutput<O extends object> = [O] extends [never] ? unknown : { readonly output: ReadonlyTaskOutput<O> };
type FinalOutput<O extends object> = [O] extends [never] ? unknown : { readonly output: O };
export interface ScratchTx {
value<T extends JsonValue>(address: Value<T>): TxValue<T>;
list<T extends JsonValue>(address: List<T>): TxList<T>;
}
export interface ScratchReader {
value<T extends JsonValue>(address: Value<T>): Pick<TxValue<T>, "get">;
list<T extends JsonValue>(address: List<T>): Pick<TxList<T>, "read">;
}
export interface TaskConversation {
readonly id: Id;
accept(options: AcceptOptions, ctx: Context): Promise<Acceptance>;
queueInput(input: QueuedInput, ctx: Context): Promise<Acceptance>;
write<E extends Entry>(
kind: EntryKind<E>,
input: EntryInput<E>,
requestId: string | undefined,
ctx: Context,
): Promise<Acceptance>;
result(inputId: Id, ctx: Context): Promise<InputResult | undefined>;
waitForInput(inputId: Id, ctx: Context): Promise<TerminalInputResult>;
abortInput(inputId: Id, ctx: Context): Promise<"aborted" | "already_placed" | "not_found">;
value<T extends JsonValue>(address: Value<T>): PublicValue<T>;
list<T extends JsonValue>(address: List<T>): PublicList<T>;
}
export interface AbortTaskConversation {
readonly id: Id;
write<E extends Entry>(
kind: EntryKind<E>,
input: EntryInput<E>,
requestId: string | undefined,
ctx: Context,
): Promise<Acceptance>;
result(inputId: Id, ctx: Context): Promise<InputResult | undefined>;
waitForInput(inputId: Id, ctx: Context): Promise<TerminalInputResult>;
value<T extends JsonValue>(address: Value<T>): PublicValue<T>;
list<T extends JsonValue>(address: List<T>): PublicList<T>;
}
export interface BaseTaskRuntime<I extends JsonValue, C extends TaskCheckpoint, O extends object = never> {
readonly taskId: Id;
commit<T>(build: (tx: BaseTaskTx<C>, current: RunningTask<I, C, O>) => T | Promise<T>, ctx: Context): Promise<T>;
scratch<T>(build: (tx: ScratchTx) => T | Promise<T>, ctx: Context): Promise<T>;
conversation(id: Id, ctx: Context): Promise<TaskConversation | undefined>;
waitForTask(id: Id, ctx: Context): Promise<Task>;
abortTask(id: Id, ctx: Context): Promise<"marked" | "terminal">;
now(): number;
sleep(untilMs: number, ctx: Context): Promise<void>;
}
export interface TurnTaskRuntime<I extends JsonValue, C extends TaskCheckpoint, O extends object = never>
extends Omit<BaseTaskRuntime<I, C, O>, "commit"> {
commit<T>(build: (tx: TurnTaskTx<C>, current: RunningTask<I, C, O>) => T | Promise<T>, ctx: Context): Promise<T>;
}
export type RuntimeFor<
I extends JsonValue,
C extends TaskCheckpoint,
O extends object,
T extends false | true,
> = (T extends true ? TurnTaskRuntime<I, C, O> : BaseTaskRuntime<I, C, O>) & RuntimeOutput<O>;
export interface TerminalTx<C extends TaskCheckpoint> extends BaseTaskTx<C> {}
export interface TurnTerminalTx<C extends TaskCheckpoint> extends TurnTaskTx<C> {}
export type FinalTx<C extends TaskCheckpoint, O extends object, T extends false | true> = (T extends true
? TurnTerminalTx<C>
: TerminalTx<C>) &
FinalOutput<O>;
export type TerminalClosure<
I extends JsonValue,
C extends TaskCheckpoint,
R extends JsonValue,
F extends JsonValue,
O extends object,
T extends false | true,
> = (tx: FinalTx<C, O, T>, current: RunningTask<I, C, O>) => Completion<R, F> | Promise<Completion<R, F>>;
export interface AbortTx<C extends TaskCheckpoint> extends TxReaders {
checkpoint(value: C): void;
value<T extends JsonValue>(address: Value<T>): TxValue<T>;
list<T extends JsonValue>(address: List<T>): TxList<T>;
write<E extends Entry>(
conversationId: Id,
kind: EntryKind<E>,
input: EntryInput<E>,
requestId?: string,
): Promise<Acceptance>;
markTask(id: Id): Promise<"marked" | "terminal">;
}
export interface TurnAbortTx<C extends TaskCheckpoint> extends AbortTx<C> {
entry<E extends Entry>(kind: EntryKind<E>, conversationId: Id, input: EntryInput<E>): Id;
}
export type AbortFinalTx<C extends TaskCheckpoint, O extends object, T extends false | true> = (T extends true
? TurnAbortTx<C>
: AbortTx<C>) &
FinalOutput<O>;
export type AbortClosure<
I extends JsonValue,
C extends TaskCheckpoint,
A extends JsonValue,
O extends object,
T extends false | true,
> = (tx: AbortFinalTx<C, O, T>, current: RunningTask<I, C, O>) => A | Promise<A>;
export interface BaseAbortRuntime<I extends JsonValue, C extends TaskCheckpoint, O extends object = never> {
readonly taskId: Id;
commit<T>(build: (tx: AbortTx<C>, current: RunningTask<I, C, O>) => T | Promise<T>, ctx: Context): Promise<T>;
scratch<T>(read: (scratch: ScratchReader) => T | Promise<T>, ctx: Context): Promise<T>;
conversation(id: Id, ctx: Context): Promise<AbortTaskConversation | undefined>;
waitForTask(id: Id, ctx: Context): Promise<Task>;
abortTask(id: Id, ctx: Context): Promise<"marked" | "terminal">;
now(): number;
}
export interface TurnAbortRuntime<I extends JsonValue, C extends TaskCheckpoint, O extends object = never>
extends Omit<BaseAbortRuntime<I, C, O>, "commit"> {
commit<T>(build: (tx: TurnAbortTx<C>, current: RunningTask<I, C, O>) => T | Promise<T>, ctx: Context): Promise<T>;
}
export type AbortRuntimeFor<
I extends JsonValue,
C extends TaskCheckpoint,
O extends object,
T extends false | true,
> = (T extends true ? TurnAbortRuntime<I, C, O> : BaseAbortRuntime<I, C, O>) & AbortRuntimeOutput<O>;

View file

@ -0,0 +1,207 @@
import type { Context } from "@earendil-works/chord";
import type { Id, JsonObject, JsonValue } from "./core.ts";
import type { AbortClosure, AbortRuntimeFor, RuntimeFor, TerminalClosure } from "./runtime.ts";
export interface TaskCheckpoint extends JsonObject {
readonly phase: string;
}
export type NoCheckpoint = never;
declare const taskOutputType: unique symbol;
export interface TaskOutputKind<O extends object> {
readonly kind: string;
readonly [taskOutputType]?: O;
}
export function defineTaskOutput<O extends object>(kind: string): TaskOutputKind<O> {
return Object.freeze({ kind });
}
export interface TaskOutputSpec<I extends JsonValue, O extends object> {
readonly kind: TaskOutputKind<O>;
initial(input: I): O;
}
export interface StoredTaskOutputRef {
readonly id: Id;
readonly kind: string;
}
export interface TaskOutputRef<O extends object> extends StoredTaskOutputRef {
readonly [taskOutputType]?: O;
}
export type TaskOutputField<O extends object> = [O] extends [never] ? unknown : { readonly output: TaskOutputRef<O> };
type TaskOutputDefinition<I extends JsonValue, O extends object> = [O] extends [never]
? { readonly output?: never }
: { readonly output: TaskOutputSpec<I, O> };
export type TaskOutcome<R extends JsonValue, F extends JsonValue, A extends JsonValue> =
| { readonly status: "completed"; readonly result: R }
| { readonly status: "failed"; readonly failure: F }
| { readonly status: "aborted"; readonly result: A }
| { readonly status: "orphaned" };
export interface TaskBase<I extends JsonValue, C extends TaskCheckpoint> {
readonly id: Id;
readonly conversationId: Id;
readonly kind: string;
readonly input: I;
readonly checkpoint?: C;
readonly after: readonly Id[];
readonly background?: true;
readonly turn?: true;
readonly owns: readonly Id[];
readonly output?: StoredTaskOutputRef;
readonly abort?: true;
}
export type Task<
I extends JsonValue = JsonValue,
C extends TaskCheckpoint = TaskCheckpoint,
R extends JsonValue = JsonValue,
F extends JsonValue = JsonValue,
A extends JsonValue = JsonValue,
> = TaskBase<I, C> &
(
| { readonly status: "pending" | "running"; readonly outcome?: never }
| { readonly status: "terminal"; readonly outcome: TaskOutcome<R, F, A> }
);
export type RunningTask<I extends JsonValue, C extends TaskCheckpoint, O extends object = never> = Omit<
TaskBase<I, C>,
"output"
> &
TaskOutputField<O> & {
readonly status: "running";
readonly outcome?: never;
};
export type Completion<R extends JsonValue, F extends JsonValue> =
| { readonly status: "completed"; readonly result: R }
| { readonly status: "failed"; readonly failure: F };
const taskKindBrand: unique symbol = Symbol("pico.taskKind");
export interface TaskKindBase {
readonly kind: string;
readonly turn: false | true;
readonly [taskKindBrand]: true;
}
interface TaskKindMethods<
I extends JsonValue,
C extends TaskCheckpoint,
R extends JsonValue,
F extends JsonValue,
A extends JsonValue,
O extends object,
T extends false | true,
> extends TaskKindBase {
readonly turn: T;
execute(
task: RunningTask<I, C, O>,
runtime: RuntimeFor<I, C, O, T>,
ctx: Context,
): Promise<TerminalClosure<I, C, R, F, O, T>>;
recover(
task: RunningTask<I, C, O>,
runtime: RuntimeFor<I, C, O, T>,
ctx: Context,
): Promise<TerminalClosure<I, C, R, F, O, T>>;
abort(
task: RunningTask<I, C, O>,
runtime: AbortRuntimeFor<I, C, O, T>,
ctx: Context,
): Promise<AbortClosure<I, C, A, O, T>>;
}
export type TaskKind<
I extends JsonValue,
C extends TaskCheckpoint,
R extends JsonValue,
F extends JsonValue,
A extends JsonValue,
O extends object = never,
T extends false | true = false,
> = TaskKindMethods<I, C, R, F, A, O, T> & TaskOutputDefinition<I, O>;
export type NoExtra<Expected, Actual extends Expected> = Actual & Record<Exclude<keyof Actual, keyof Expected>, never>;
export type ExactJsonInput<Expected extends JsonValue, Actual extends Expected> = Expected extends readonly JsonValue[]
? Actual
: Expected extends JsonObject
? Actual & Record<Exclude<keyof Actual, keyof Expected>, never>
: Actual;
export type NonTurnKindDefinition<
I extends JsonValue,
C extends TaskCheckpoint,
R extends JsonValue,
F extends JsonValue,
A extends JsonValue,
O extends object,
> = Omit<TaskKind<I, C, R, F, A, O, false>, "turn" | typeof taskKindBrand> & { readonly turn?: false };
export type TurnKindDefinition<
I extends JsonValue,
C extends TaskCheckpoint,
R extends JsonValue,
F extends JsonValue,
A extends JsonValue,
O extends object,
> = Omit<TaskKind<I, C, R, F, A, O, true>, typeof taskKindBrand>;
export interface TaskKindFactory<
I extends JsonValue,
C extends TaskCheckpoint,
R extends JsonValue,
F extends JsonValue,
A extends JsonValue,
O extends object,
> {
<D extends NonTurnKindDefinition<I, C, R, F, A, O>>(
definition: NoExtra<NonTurnKindDefinition<I, C, R, F, A, O>, D>,
): D & TaskKind<I, C, R, F, A, O, false>;
<D extends TurnKindDefinition<I, C, R, F, A, O>>(
definition: NoExtra<TurnKindDefinition<I, C, R, F, A, O>, D>,
): D & TaskKind<I, C, R, F, A, O, true>;
}
export function defineTask<
I extends JsonValue,
C extends TaskCheckpoint,
R extends JsonValue,
F extends JsonValue,
A extends JsonValue,
O extends object = never,
>(): TaskKindFactory<I, C, R, F, A, O> {
const define = (definition: NonTurnKindDefinition<I, C, R, F, A, O> | TurnKindDefinition<I, C, R, F, A, O>) =>
Object.freeze({
...definition,
turn: definition.turn ?? false,
[taskKindBrand]: true as const,
});
return define as TaskKindFactory<I, C, R, F, A, O>;
}
export type AnyTaskKind = TaskKindBase;
export type PayloadsOf<K> = K extends TaskKind<infer I, infer C, infer R, infer F, infer A, infer O, false | true>
? { input: I; checkpoint: C; result: R; failure: F; aborted: A; output: O }
: never;
export type InputOf<K> = PayloadsOf<K>["input"];
export type CheckpointOf<K> = PayloadsOf<K>["checkpoint"];
export type ResultOf<K> = PayloadsOf<K>["result"];
export type FailureOf<K> = PayloadsOf<K>["failure"];
export type AbortedOf<K> = PayloadsOf<K>["aborted"];
export type OutputOf<K> = PayloadsOf<K>["output"];
export type TurnOf<K extends TaskKindBase> = K["turn"];
type WithoutOutput<T> = T extends unknown ? Omit<T, "output"> : never;
export type TaskOf<K extends TaskKindBase> = WithoutOutput<
Task<InputOf<K>, CheckpointOf<K>, ResultOf<K>, FailureOf<K>, AbortedOf<K>>
> &
TaskOutputField<OutputOf<K>>;

View file

@ -0,0 +1,7 @@
{
"extends": "../../../../../tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["../../../src/harness/pico/**/*.ts", "./types.compile.ts"]
}

View file

@ -0,0 +1,379 @@
import type { Context } from "@earendil-works/chord";
import type {
AbortClosure,
AbortedOf,
AbortRuntimeFor,
BaseAbortRuntime,
BaseTaskRuntime,
BaseTaskTx,
CheckpointOf,
Entry,
EntryInput,
ExactJsonInput,
FailureOf,
InputOf,
JsonValue,
List,
OutputOf,
PayloadsOf,
ReadonlyTaskOutput,
ResultOf,
RuntimeFor,
SummaryEntry,
TaskOf,
TaskOutcome,
TaskOutput,
TaskOutputRef,
TerminalClosure,
TurnAbortRuntime,
TurnOf,
TurnTaskRuntime,
Value,
} from "../../../src/harness/pico/index.ts";
import { defineEntry, defineList, defineTask, defineTaskOutput, defineValue } from "../../../src/harness/pico/index.ts";
type Equal<Left, Right> = (<Type>() => Type extends Left ? 1 : 2) extends <Type>() => Type extends Right ? 1 : 2
? (<Type>() => Type extends Right ? 1 : 2) extends <Type>() => Type extends Left ? 1 : 2
? true
: false
: false;
type Assert<Condition extends true> = Condition;
type HasEntry<Surface> = "entry" extends keyof Surface ? true : false;
type HasOutput<Surface> = "output" extends keyof Surface ? true : false;
type TestInput = {
readonly prompt: string;
readonly nested: { readonly count: number };
};
type TestCheckpoint = {
readonly phase: "ready";
readonly cursor: number;
};
type SameCheckpoint = {
readonly phase: "ready";
readonly cursor: number;
};
type IncompatibleCheckpoint = {
readonly phase: "ready";
readonly token: string;
};
type TestResult = { readonly answer: string };
type TestFailure = { readonly code: "failed" };
type TestAborted = { readonly cleaned: boolean };
type TestOutput = { text: string; chunks: string[] };
type JsonAssertions = [
Assert<TestInput extends JsonValue ? true : false>,
Assert<readonly [string, { readonly value: number }] extends JsonValue ? true : false>,
Assert<Date extends JsonValue ? false : true>,
Assert<{ readonly value: undefined } extends JsonValue ? false : true>,
];
const nonTurnKind = defineTask<TestInput, TestCheckpoint, TestResult, TestFailure, TestAborted>()({
kind: "test.non_turn",
execute() {
return Promise.resolve(() => ({ status: "completed", result: { answer: "done" } }));
},
recover() {
return Promise.resolve(() => ({ status: "failed", failure: { code: "failed" } }));
},
abort() {
return Promise.resolve(() => ({ cleaned: true }));
},
});
const turnKind = defineTask<TestInput, TestCheckpoint, TestResult, TestFailure, TestAborted>()({
kind: "test.turn",
turn: true,
execute() {
return Promise.resolve(() => ({ status: "completed", result: { answer: "done" } }));
},
recover() {
return Promise.resolve(() => ({ status: "failed", failure: { code: "failed" } }));
},
abort() {
return Promise.resolve(() => ({ cleaned: true }));
},
});
const sameCheckpointKind = defineTask<TestInput, SameCheckpoint, TestResult, TestFailure, TestAborted>()({
kind: "test.same_checkpoint",
execute() {
return Promise.resolve(() => ({ status: "completed", result: { answer: "done" } }));
},
recover() {
return Promise.resolve(() => ({ status: "failed", failure: { code: "failed" } }));
},
abort() {
return Promise.resolve(() => ({ cleaned: true }));
},
});
const testOutputKind = defineTaskOutput<TestOutput>("test.output");
const outputTaskKind = defineTask<TestInput, TestCheckpoint, TestResult, TestFailure, TestAborted, TestOutput>()({
kind: "test.with_output",
output: {
kind: testOutputKind,
initial(input) {
return { text: input.prompt, chunks: [] };
},
},
execute() {
return Promise.resolve(() => ({ status: "completed", result: { answer: "done" } }));
},
recover() {
return Promise.resolve(() => ({ status: "failed", failure: { code: "failed" } }));
},
abort() {
return Promise.resolve(() => ({ cleaned: true }));
},
});
const incompatibleCheckpointKind = defineTask<
TestInput,
IncompatibleCheckpoint,
TestResult,
TestFailure,
TestAborted
>()({
kind: "test.incompatible_checkpoint",
execute() {
return Promise.resolve(() => ({ status: "completed", result: { answer: "done" } }));
},
recover() {
return Promise.resolve(() => ({ status: "failed", failure: { code: "failed" } }));
},
abort() {
return Promise.resolve(() => ({ cleaned: true }));
},
});
type NonTurnRuntimeTx = Parameters<Parameters<RuntimeFor<TestInput, TestCheckpoint, never, false>["commit"]>[0]>[0];
type TurnRuntimeTx = Parameters<Parameters<RuntimeFor<TestInput, TestCheckpoint, never, true>["commit"]>[0]>[0];
type NonTurnAbortTx = Parameters<Parameters<AbortRuntimeFor<TestInput, TestCheckpoint, never, false>["commit"]>[0]>[0];
type TurnRuntimeAbortTx = Parameters<
Parameters<AbortRuntimeFor<TestInput, TestCheckpoint, never, true>["commit"]>[0]
>[0];
type NonTurnFinalTx = Parameters<TerminalClosure<TestInput, TestCheckpoint, TestResult, TestFailure, never, false>>[0];
type TurnFinalTx = Parameters<TerminalClosure<TestInput, TestCheckpoint, TestResult, TestFailure, never, true>>[0];
type NonTurnAbortFinalTx = Parameters<AbortClosure<TestInput, TestCheckpoint, TestAborted, never, false>>[0];
type TurnAbortFinalTx = Parameters<AbortClosure<TestInput, TestCheckpoint, TestAborted, never, true>>[0];
type OutputFinalTx = Parameters<
TerminalClosure<TestInput, TestCheckpoint, TestResult, TestFailure, TestOutput, false>
>[0];
type OutputAbortFinalTx = Parameters<AbortClosure<TestInput, TestCheckpoint, TestAborted, TestOutput, false>>[0];
type KindAssertions = [
Assert<Equal<TurnOf<typeof nonTurnKind>, false>>,
Assert<Equal<TurnOf<typeof turnKind>, true>>,
Assert<Equal<InputOf<typeof nonTurnKind>, TestInput>>,
Assert<Equal<CheckpointOf<typeof nonTurnKind>, TestCheckpoint>>,
Assert<Equal<ResultOf<typeof nonTurnKind>, TestResult>>,
Assert<Equal<FailureOf<typeof nonTurnKind>, TestFailure>>,
Assert<Equal<AbortedOf<typeof nonTurnKind>, TestAborted>>,
Assert<Equal<PayloadsOf<typeof nonTurnKind>["aborted"], TestAborted>>,
Assert<Equal<OutputOf<typeof nonTurnKind>, never>>,
Assert<Equal<OutputOf<typeof outputTaskKind>, TestOutput>>,
Assert<Equal<Parameters<typeof nonTurnKind.execute>[1], BaseTaskRuntime<TestInput, TestCheckpoint>>>,
Assert<Equal<Parameters<typeof outputTaskKind.execute>[1]["output"], TaskOutput<TestOutput>>>,
Assert<Equal<ReturnType<Parameters<TaskOutput<TestOutput>["mutate"]>[0]>, undefined>>,
Assert<Equal<Parameters<typeof turnKind.execute>[1], TurnTaskRuntime<TestInput, TestCheckpoint>>>,
Assert<Equal<Parameters<typeof nonTurnKind.abort>[1], BaseAbortRuntime<TestInput, TestCheckpoint>>>,
Assert<Equal<Parameters<typeof turnKind.abort>[1], TurnAbortRuntime<TestInput, TestCheckpoint>>>,
Assert<Equal<Parameters<typeof outputTaskKind.abort>[1]["output"], ReadonlyTaskOutput<TestOutput>>>,
Assert<Equal<HasOutput<Parameters<typeof nonTurnKind.execute>[1]>, false>>,
Assert<Equal<HasOutput<Parameters<typeof outputTaskKind.execute>[1]>, true>>,
Assert<Equal<OutputFinalTx["output"], TestOutput>>,
Assert<Equal<OutputAbortFinalTx["output"], TestOutput>>,
Assert<Equal<HasEntry<NonTurnRuntimeTx>, false>>,
Assert<Equal<HasEntry<TurnRuntimeTx>, true>>,
Assert<Equal<HasEntry<NonTurnFinalTx>, false>>,
Assert<Equal<HasEntry<TurnFinalTx>, true>>,
Assert<Equal<HasEntry<NonTurnAbortTx>, false>>,
Assert<Equal<HasEntry<TurnRuntimeAbortTx>, true>>,
Assert<Equal<HasEntry<NonTurnAbortFinalTx>, false>>,
Assert<Equal<HasEntry<TurnAbortFinalTx>, true>>,
Assert<"sleep" extends keyof BaseAbortRuntime<TestInput, TestCheckpoint> ? false : true>,
];
type KindOutcome = Extract<TaskOf<typeof nonTurnKind>, { readonly status: "terminal" }>["outcome"];
type OutcomeAssertions = [
Assert<Equal<KindOutcome, TaskOutcome<TestResult, TestFailure, TestAborted>>>,
Assert<Equal<Extract<KindOutcome, { readonly status: "completed" }>["result"], TestResult>>,
Assert<Equal<Extract<KindOutcome, { readonly status: "failed" }>["failure"], TestFailure>>,
Assert<Equal<Extract<KindOutcome, { readonly status: "aborted" }>["result"], TestAborted>>,
];
const summaryKind = defineEntry<SummaryEntry>("pi.summary");
type SummaryInput = EntryInput<SummaryEntry>;
type EntryAssertions = [
Assert<Equal<typeof summaryKind.kind, string>>,
Assert<Equal<SummaryInput["head"], number | "self">>,
Assert<"id" extends keyof SummaryInput ? false : true>,
Assert<"conversationId" extends keyof SummaryInput ? false : true>,
Assert<"kind" extends keyof SummaryInput ? false : true>,
Assert<"byTaskId" extends keyof SummaryInput ? false : true>,
];
function verifyTaskInputs(
tx: BaseTaskTx<TestCheckpoint>,
outputRef: TaskOutputRef<TestOutput>,
wrongOutputRef: TaskOutputRef<{ count: number }>,
): void {
tx.task(nonTurnKind, {
input: { prompt: "hello", nested: { count: 1 } },
});
tx.task(outputTaskKind, {
input: { prompt: "own output", nested: { count: 1 } },
});
tx.task(outputTaskKind, {
input: { prompt: "shared output", nested: { count: 1 } },
output: outputRef,
});
// @ts-expect-error a no-output kind cannot receive an output reference
tx.task(nonTurnKind, { input: { prompt: "hello", nested: { count: 1 } }, output: outputRef });
tx.task(outputTaskKind, {
input: { prompt: "wrong output", nested: { count: 1 } },
// @ts-expect-error a shared output reference must have the kind's output type
output: wrongOutputRef,
});
const exactVariable = {
conversationId: 1,
input: { prompt: "hello", nested: { count: 1 } },
background: true,
} as const;
tx.task(nonTurnKind, exactVariable);
// Top-level exactness intentionally does not recursively reject extra nested fields.
tx.task(nonTurnKind, {
input: { prompt: "hello", nested: { count: 1, extension: true } },
});
// @ts-expect-error task input literals reject extra top-level fields
tx.task(nonTurnKind, { input: { prompt: "hello", nested: { count: 1 }, extension: true } });
const extraInputVariable = { prompt: "hello", nested: { count: 1 }, extension: true };
// @ts-expect-error task input variables reject visible extra top-level fields
tx.task(nonTurnKind, { input: extraInputVariable });
const inputBase = { prompt: "hello", nested: { count: 1 } };
// @ts-expect-error task input spreads reject visible extra top-level fields
tx.task(nonTurnKind, { input: { ...inputBase, extension: true } });
const extraSpecVariable = {
input: { prompt: "hello", nested: { count: 1 } },
priority: 1,
};
// @ts-expect-error task specs reject visible extra top-level fields
tx.task(nonTurnKind, extraSpecVariable);
tx.checkpoint({ phase: "ready", cursor: 1 });
// @ts-expect-error every checkpoint requires its phase
tx.checkpoint({ cursor: 1 });
// @ts-expect-error checkpoint replacement requires the complete kind-specific shape
tx.checkpoint({ phase: "ready" });
// @ts-expect-error an incompatible checkpoint shape is rejected
tx.checkpoint({ phase: "ready", token: "cursor" });
}
function verifyOutputMutation(
output: TaskOutput<TestOutput>,
readonlyOutput: ReadonlyTaskOutput<TestOutput>,
ctx: Context,
): void {
void output.mutate((state) => {
state.chunks.push("chunk");
}, ctx);
// @ts-expect-error task-output mutation callbacks must be synchronous
void output.mutate(async (state) => {
state.chunks.push("chunk");
}, ctx);
// @ts-expect-error fresh abort output is read-only
void readonlyOutput.mutate;
}
function verifyCheckpointCompatibility(
checkpoint: CheckpointOf<typeof sameCheckpointKind>,
incompatible: CheckpointOf<typeof incompatibleCheckpointKind>,
): void {
// Checkpoints are structural: an independently declared identical shape is assignable.
const structurallyCompatible: CheckpointOf<typeof nonTurnKind> = checkpoint;
void structurallyCompatible;
// @ts-expect-error a differently shaped kind checkpoint is not assignable
const structurallyIncompatible: CheckpointOf<typeof nonTurnKind> = incompatible;
void structurallyIncompatible;
}
function verifyOutcomes(outcome: KindOutcome): void {
const completed: KindOutcome = { status: "completed", result: { answer: "done" } };
const failed: KindOutcome = { status: "failed", failure: { code: "failed" } };
const aborted: KindOutcome = { status: "aborted", result: { cleaned: true } };
const orphaned: KindOutcome = { status: "orphaned" };
// @ts-expect-error completed outcomes require this kind's result payload
const wrongCompleted: KindOutcome = { status: "completed", result: { answer: 1 } };
// @ts-expect-error failed outcomes require this kind's failure payload
const wrongFailure: KindOutcome = { status: "failed", failure: { code: "other" } };
// @ts-expect-error aborted outcomes require this kind's abort payload
const wrongAborted: KindOutcome = { status: "aborted", result: { cleaned: "yes" } };
void outcome;
void completed;
void failed;
void aborted;
void orphaned;
void wrongCompleted;
void wrongFailure;
void wrongAborted;
}
function verifyAddresses(): void {
const sessionValue = defineValue<string>({ type: "session" }, "plugin.name");
const taskList = defineList<number>({ type: "task", taskId: 2 }, "plugin.items", { key: "active" });
const sharedList = defineList<number>({ type: "shared", id: 4 }, "plugin.shared");
const conversationValue = defineValue<TestResult>({ type: "conversation", conversationId: 3 }, "plugin.result", {
rewind: true,
});
const conversationList = defineList<string>({ type: "conversation", conversationId: 3 }, "plugin.events", {
rewind: false,
});
const typedSessionValue: Value<string> = sessionValue;
const typedTaskList: List<number> = taskList;
void typedSessionValue;
void typedTaskList;
void sharedList;
void conversationValue;
void conversationList;
// @ts-expect-error conversation addresses explicitly select rewind behavior
defineValue<string>({ type: "conversation", conversationId: 3 }, "plugin.value");
// @ts-expect-error session addresses are sticky
defineValue<string>({ type: "session" }, "plugin.value", { rewind: true });
// @ts-expect-error task addresses are never rewindable
defineList<string>({ type: "task", taskId: 2 }, "plugin.list", { rewind: true });
// @ts-expect-error shared addresses are never rewindable
defineList<string>({ type: "shared", id: 4 }, "plugin.shared", { rewind: true });
// @ts-expect-error address payloads are strict JSON
defineValue<Date>({ type: "session" }, "plugin.date");
}
type ExactInputAssertion = Assert<
Equal<ExactJsonInput<TestInput, TestInput & { readonly extension: boolean }>["extension"], never>
>;
declare const jsonAssertions: JsonAssertions;
declare const kindAssertions: KindAssertions;
declare const outcomeAssertions: OutcomeAssertions;
declare const entryAssertions: EntryAssertions;
declare const exactInputAssertion: ExactInputAssertion;
declare const entry: Entry;
void jsonAssertions;
void kindAssertions;
void outcomeAssertions;
void entryAssertions;
void exactInputAssertion;
void entry;
void verifyTaskInputs;
void verifyOutputMutation;
void verifyCheckpointCompatibility;
void verifyOutcomes;
void verifyAddresses;