mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(agent): add pico compile-time foundation
This commit is contained in:
parent
4819cc877e
commit
f467cf7068
10 changed files with 1507 additions and 291 deletions
|
|
@ -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
77
packages/agent/src/harness/pico/addresses.ts
Normal file
77
packages/agent/src/harness/pico/addresses.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
8
packages/agent/src/harness/pico/core.ts
Normal file
8
packages/agent/src/harness/pico/core.ts
Normal 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;
|
||||
66
packages/agent/src/harness/pico/entries.ts
Normal file
66
packages/agent/src/harness/pico/entries.ts
Normal 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;
|
||||
5
packages/agent/src/harness/pico/index.ts
Normal file
5
packages/agent/src/harness/pico/index.ts
Normal 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";
|
||||
300
packages/agent/src/harness/pico/runtime.ts
Normal file
300
packages/agent/src/harness/pico/runtime.ts
Normal 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>;
|
||||
207
packages/agent/src/harness/pico/tasks.ts
Normal file
207
packages/agent/src/harness/pico/tasks.ts
Normal 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>>;
|
||||
7
packages/agent/test/harness/pico/tsconfig.json
Normal file
7
packages/agent/test/harness/pico/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["../../../src/harness/pico/**/*.ts", "./types.compile.ts"]
|
||||
}
|
||||
379
packages/agent/test/harness/pico/types.compile.ts
Normal file
379
packages/agent/test/harness/pico/types.compile.ts
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue