@agentswf/contract 0.0.0-stage → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ import type { JsonObject, JsonValue } from "./json";
2
+ import type { AbsoluteDeadline } from "./timing";
3
+
4
+ /** Instructions, an option's description or a level: text, or structured text, as Jev allows. */
5
+ export type DecisionText = string | JsonObject;
6
+
7
+ /** An option's name and its description, or `null` when the name says enough. */
8
+ export type DecisionOptions = Record<string, DecisionText | null>;
9
+ export type DecisionLevels = readonly DecisionText[];
10
+
11
+ export interface ChoiceQuestion<O extends DecisionOptions = DecisionOptions> {
12
+ readonly type: "choice";
13
+ readonly instructions: DecisionText;
14
+ readonly options: O;
15
+ }
16
+
17
+ export interface ScoreQuestion<L extends DecisionLevels = DecisionLevels> {
18
+ readonly type: "score";
19
+ readonly instructions: DecisionText;
20
+ /** Ordered, lowest first. */
21
+ readonly levels: L;
22
+ }
23
+
24
+ export interface YesNoQuestion {
25
+ readonly type: "yes-no";
26
+ readonly instructions: DecisionText;
27
+ readonly criteria?: { readonly yes: DecisionText; readonly no: DecisionText };
28
+ }
29
+
30
+ export type Question = ChoiceQuestion | ScoreQuestion | YesNoQuestion;
31
+
32
+ /** Picks one of `options`. Its answer is typed by their names. */
33
+ export function choice<const O extends DecisionOptions>(
34
+ instructions: DecisionText,
35
+ options: O,
36
+ ): ChoiceQuestion<O> {
37
+ assertInstructions(instructions);
38
+ if (Object.keys(options).length === 0) throw new Error("a choice needs at least one option");
39
+ return { type: "choice", instructions, options };
40
+ }
41
+
42
+ /** Places the state on `levels`, lowest first. Its answer is typed by their indices. */
43
+ export function score<const L extends DecisionLevels>(
44
+ instructions: DecisionText,
45
+ levels: L,
46
+ ): ScoreQuestion<L> {
47
+ assertInstructions(instructions);
48
+ if (levels.length === 0) throw new Error("a score needs at least one level");
49
+ return { type: "score", instructions, levels };
50
+ }
51
+
52
+ /** The probability that the answer is yes. */
53
+ export function yesNo(
54
+ instructions: DecisionText,
55
+ criteria?: { yes: DecisionText; no: DecisionText },
56
+ ): YesNoQuestion {
57
+ assertInstructions(instructions);
58
+ return criteria ? { type: "yes-no", instructions, criteria } : { type: "yes-no", instructions };
59
+ }
60
+
61
+ function assertInstructions(instructions: DecisionText): void {
62
+ if (typeof instructions === "string" && instructions.trim() === "") {
63
+ throw new Error("a question needs instructions");
64
+ }
65
+ }
66
+
67
+ /** A tuple's indices as numbers, or `number` for levels only known at run time. */
68
+ type LevelOf<L extends DecisionLevels> = number extends L["length"]
69
+ ? number
70
+ : Extract<keyof L, `${number}`> extends `${infer N extends number}`
71
+ ? N
72
+ : never;
73
+
74
+ type ChoiceAnswer<O extends DecisionOptions = DecisionOptions> = {
75
+ readonly type: "choice";
76
+ /** The most likely option; the first of them on a tie. */
77
+ readonly choice: keyof O & string;
78
+ readonly probabilities: { readonly [K in keyof O & string]: number };
79
+ };
80
+
81
+ type ScoreAnswer<L extends DecisionLevels = DecisionLevels> = {
82
+ readonly type: "score";
83
+ /** The most likely level; the lowest of them on a tie. */
84
+ readonly level: LevelOf<L>;
85
+ /** The probability-weighted level. */
86
+ readonly expected: number;
87
+ /** One per level, lowest first. */
88
+ readonly probabilities: readonly number[];
89
+ };
90
+
91
+ type YesNoAnswer = { readonly type: "yes-no"; readonly yes: number };
92
+
93
+ /**
94
+ * Every answer carries its whole distribution, as the provider gave it: rounded, and not
95
+ * renormalised. The top pick is a convenience.
96
+ */
97
+ export type Answer<Q extends Question> =
98
+ Q extends ChoiceQuestion<infer O>
99
+ ? ChoiceAnswer<O>
100
+ : Q extends ScoreQuestion<infer L>
101
+ ? ScoreAnswer<L>
102
+ : YesNoAnswer;
103
+
104
+ export type Answers<Q extends Record<string, Question>> = { readonly [K in keyof Q]: Answer<Q[K]> };
105
+
106
+ /** A decision model alias the operator configured, such as `jev`. */
107
+ export type DecisionAliasName = string;
108
+
109
+ export interface DecisionSpec<Q extends Record<string, Question> = Record<string, Question>> {
110
+ /** Names the call in its record, and its prefix before `:` is its accounting stage. Not unique. */
111
+ key: string;
112
+ model: DecisionAliasName;
113
+ /** What every question is asked about. The provider bills it once per call, however many questions. */
114
+ state: string | JsonObject | JsonValue[];
115
+ /** At least one. They are answered independently and do not see each other's answers. */
116
+ questions: Q;
117
+ /** Defaults to the current scope's deadline, and can only be earlier. */
118
+ deadline?: AbsoluteDeadline;
119
+ }
120
+
121
+ /** One `decide` call, as the run keeps it in `output.json`. */
122
+ export type DecisionRecord = {
123
+ callPath: string[];
124
+ key: string;
125
+ /** The workflow stage it was asked in; absent between stages. */
126
+ stage?: string;
127
+ /** The alias the workflow asked for. */
128
+ alias: DecisionAliasName;
129
+ provider: string;
130
+ /** As the alias resolved it. */
131
+ model: string;
132
+ /** The versioned model that answered; absent when none did. A fitted threshold holds only for it. */
133
+ snapshot?: string;
134
+ startedAt: string;
135
+ settledAt: string;
136
+ questions: { id: string; type: Question["type"] }[];
137
+ /**
138
+ * SHA-256 of the questions: types, instructions, options, levels and criteria. With `snapshot`,
139
+ * it is what a fitted threshold binds to.
140
+ */
141
+ questionsDigest: string;
142
+ outcome: "answered" | "failed" | "timed-out" | "cancelled";
143
+ error?: string;
144
+ /** Requests sent, retries included; each may have been billed. */
145
+ attempts: number;
146
+ /** Summed over attempts, as far as the provider reported them. */
147
+ tokens?: { input: number; output: number };
148
+ requestId?: string;
149
+ /** The request and its answers, relative to the run's artifacts, for fitting thresholds offline. */
150
+ artifact: string;
151
+ };
152
+
153
+ export interface DecisionDirectory {
154
+ /**
155
+ * One request: the state once, every question about it. Once sent, a call is recorded whatever
156
+ * its outcome, and rejects with `DeadlineExceededError` past its deadline or `DecisionError`
157
+ * otherwise. A spec the engine refuses (an unknown alias, no questions) rejects with an `Error`
158
+ * before anything is sent, and leaves no record.
159
+ */
160
+ decide<const Q extends Record<string, Question>>(
161
+ spec: DecisionSpec<Q>,
162
+ ): Promise<{ answers: Answers<Q>; record: DecisionRecord }>;
163
+ }
164
+
165
+ /** A decision that was asked and did not answer. Its record is in the run's too. */
166
+ export class DecisionError extends Error {
167
+ readonly code = "decision-failed" as const;
168
+
169
+ constructor(
170
+ message: string,
171
+ readonly record: DecisionRecord,
172
+ ) {
173
+ super(message);
174
+ this.name = "DecisionError";
175
+ }
176
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ type ChoiceQuestion,
3
+ choice,
4
+ type DecisionSpec,
5
+ type Question,
6
+ score,
7
+ type WorkflowContext,
8
+ yesNo,
9
+ } from "./index";
10
+
11
+ declare const context: WorkflowContext;
12
+ declare const issues: { id: string; mechanism: string }[];
13
+
14
+ type Equal<A, B> =
15
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
16
+ function expectType<T extends true>(_proof?: T): void {}
17
+
18
+ async function inline(): Promise<void> {
19
+ const { answers, record } = await context.decisions.decide({
20
+ key: "triage:1",
21
+ model: "jev",
22
+ state: { ticket: "Checkout double-charges on retry" },
23
+ questions: {
24
+ team: choice("Which team owns `ticket`?", { payments: "Checkout, billing", frontend: null }),
25
+ bug: yesNo("Does `ticket` report broken behaviour?"),
26
+ urgency: score("How urgent is `ticket`?", ["later", "this week", "now"]),
27
+ },
28
+ });
29
+ expectType<Equal<typeof answers.team.choice, "payments" | "frontend">>();
30
+ expectType<Equal<typeof answers.urgency.level, 0 | 1 | 2>>();
31
+ expectType<Equal<typeof answers.urgency.probabilities, readonly number[]>>();
32
+ expectType<Equal<typeof answers.bug.yes, number>>();
33
+ const payments: number = answers.team.probabilities.payments;
34
+ const expected: number = answers.urgency.expected;
35
+ const snapshot: string | undefined = record.snapshot;
36
+ // @ts-expect-error Not an option.
37
+ void answers.team.probabilities.account;
38
+ // @ts-expect-error Not a question.
39
+ void answers.severity;
40
+ // @ts-expect-error A yes-no has no pick of its own.
41
+ void answers.bug.choice;
42
+ void [payments, expected, snapshot];
43
+ }
44
+
45
+ /** A decision reused across workflows: a function that returns the call. */
46
+ function matchIssue(text: string) {
47
+ const options = Object.fromEntries(issues.map((issue) => [issue.id, issue.mechanism]));
48
+ return {
49
+ key: "match:fixture",
50
+ model: "jev",
51
+ state: { finding: text },
52
+ questions: { issue: choice("Which known problem does `finding` raise?", options) },
53
+ };
54
+ }
55
+
56
+ const SEVERITY = score("How severe is `issue`?", ["nit", "could-fix", "should-fix", "must-fix"]);
57
+
58
+ async function reused(): Promise<void> {
59
+ const { answers } = await context.decisions.decide(matchIssue("a finding"));
60
+ // Options only known at run time widen to `string`.
61
+ expectType<Equal<typeof answers.issue.choice, string>>();
62
+ const graded = await context.decisions.decide({
63
+ key: "grade:1",
64
+ model: "jev",
65
+ state: "an issue",
66
+ questions: { severity: SEVERITY },
67
+ });
68
+ expectType<Equal<typeof graded.answers.severity.level, 0 | 1 | 2 | 3>>();
69
+ const dynamic = await context.decisions.decide({
70
+ key: "grade:2",
71
+ model: "jev",
72
+ state: "an issue",
73
+ questions: {
74
+ severity: score(
75
+ "How severe?",
76
+ issues.map((issue) => issue.id),
77
+ ),
78
+ },
79
+ });
80
+ expectType<Equal<typeof dynamic.answers.severity.level, number>>();
81
+ }
82
+
83
+ function rejectedShapes(): void {
84
+ // @ts-expect-error A question's type is one of three.
85
+ const unknownType: Question = { type: "rank", instructions: "Rank it" };
86
+ // @ts-expect-error Questions are required.
87
+ const noQuestions: DecisionSpec = { key: "k", model: "jev", state: "s" };
88
+ // A question written as a bare literal outside the call needs `satisfies`, or its type widens.
89
+ const bare = {
90
+ type: "choice",
91
+ instructions: "Which?",
92
+ options: { a: null },
93
+ } satisfies ChoiceQuestion;
94
+ void [unknownType, noQuestions, bare];
95
+ }
96
+
97
+ void [inline, reused, rejectedShapes];
@@ -0,0 +1,87 @@
1
+ import type { JsonValue } from "./json";
2
+ import type { StageOutcome, WorkflowDefinition } from "./workflow";
3
+
4
+ export const EXECUTABLE_WORKFLOW_KIND = "awf.executable-workflow/v1" as const;
5
+
6
+ export type WorkflowInvocation = Readonly<{
7
+ argv: readonly string[];
8
+ cwd: string;
9
+ }>;
10
+
11
+ /** One stage an attempt entered: run, or reused from the attempt that ran it. */
12
+ export type StageSummary = {
13
+ stage: string;
14
+ source: "ran" | "reused";
15
+ outcome: StageOutcome;
16
+ /** The attempt that ran it. */
17
+ attempt: number;
18
+ /** How long it ran in this attempt: zero for a reused one. */
19
+ spanMs: number;
20
+ summary?: string;
21
+ /** Its value, for a stage that returns one and succeeded. */
22
+ value?: JsonValue;
23
+ /** Reused from a value given rather than one a turn produced; see `StageRecord.provided`. */
24
+ provided?: true;
25
+ };
26
+
27
+ /** How an attempt ended. `interrupted` is never written: it is an attempt with no ending and no process. */
28
+ export type AttemptOutcome = "completed" | UnfinishedOutcome;
29
+
30
+ /**
31
+ * How an attempt that didn't complete ended. `cancelled` is the operator stopping it, and wins over
32
+ * the others. `timed-out` is its own deadline ending it; a deadline the workflow set and let escape
33
+ * is `failed`. `stopped` is a stop: the workflow's, or a continue that can't go on as asked.
34
+ */
35
+ export type UnfinishedOutcome = "stopped" | "failed" | "timed-out" | "cancelled";
36
+
37
+ /** How an attempt ended, which `report` renders. */
38
+ export type Ending<Result> =
39
+ | { kind: "completed"; value: Result; stages: StageSummary[] }
40
+ | {
41
+ kind: UnfinishedOutcome;
42
+ /** The stage it ended in; absent between stages. */
43
+ stage?: string;
44
+ reason: string;
45
+ stages: StageSummary[];
46
+ /** The command that goes on from here. */
47
+ continue: string;
48
+ };
49
+
50
+ /**
51
+ * Adapts operator input to one programmatic workflow call, and its result back to the operator,
52
+ * without owning runtime configuration.
53
+ */
54
+ export interface ExecutableWorkflow<Args extends JsonValue, Result extends JsonValue> {
55
+ readonly kind: typeof EXECUTABLE_WORKFLOW_KIND;
56
+ readonly definition: WorkflowDefinition<Args, Result>;
57
+ prepare(invocation: WorkflowInvocation): Args;
58
+ /**
59
+ * The run's id, derived from its args, such as a ticket's key: unique within the workflow,
60
+ * letters, digits, `.`, `_` and `-`. `awf run --id` overrides it; without either, one is
61
+ * generated.
62
+ */
63
+ id?(args: Args): string;
64
+ /**
65
+ * Renders a completed attempt's value for a person at a terminal. Undefined, or no `present`, and
66
+ * awf prints the JSON. An attempt that didn't complete is awf's to print: its stage, its reason
67
+ * and the command that goes on.
68
+ */
69
+ present?(value: Result, ending: Ending<Result>): string | undefined;
70
+ /**
71
+ * A Markdown handoff of how the attempt ended for whoever acts on it, which the operator saves as
72
+ * report.md. Called for every ending, `value` only for a completed one, so a stopped attempt can
73
+ * hand off what its stages found. Undefined writes none.
74
+ */
75
+ report?(value: Result | undefined, ending: Ending<Result>): string | undefined;
76
+ }
77
+
78
+ type ExecutableWorkflowSpec<Args extends JsonValue, Result extends JsonValue> = Omit<
79
+ ExecutableWorkflow<Args, Result>,
80
+ "kind"
81
+ >;
82
+
83
+ export function defineExecutableWorkflow<Args extends JsonValue, Result extends JsonValue>(
84
+ spec: ExecutableWorkflowSpec<Args, Result>,
85
+ ): ExecutableWorkflow<Args, Result> {
86
+ return { ...spec, kind: EXECUTABLE_WORKFLOW_KIND };
87
+ }
@@ -0,0 +1,10 @@
1
+ export * from "./agents";
2
+ export * from "./composition";
3
+ export * from "./decisions";
4
+ export * from "./executable";
5
+ export * from "./json";
6
+ export * from "./messaging";
7
+ export * from "./participants";
8
+ export * from "./sandboxes";
9
+ export * from "./timing";
10
+ export * from "./workflow";
@@ -0,0 +1,74 @@
1
+ export type JsonPrimitive = string | number | boolean | null;
2
+
3
+ export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
4
+
5
+ export type JsonObject = { [key: string]: JsonValue };
6
+
7
+ export function isJsonValue(value: unknown): value is JsonValue {
8
+ return isJsonValueAt(value, new Set<object>());
9
+ }
10
+
11
+ function isJsonValueAt(value: unknown, ancestors: Set<object>): value is JsonValue {
12
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
13
+ if (typeof value === "number") return Number.isFinite(value);
14
+ if (typeof value !== "object") return false;
15
+ if (ancestors.has(value)) return false;
16
+ ancestors.add(value);
17
+ try {
18
+ if (Array.isArray(value)) {
19
+ const keys = Reflect.ownKeys(value);
20
+ if (keys.length !== value.length + 1 || !keys.includes("length")) return false;
21
+ return keys.every((key) => {
22
+ if (key === "length") return true;
23
+ if (!isArrayIndex(key)) return false;
24
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
25
+ return (
26
+ descriptor?.enumerable === true &&
27
+ "value" in descriptor &&
28
+ isJsonValueAt(descriptor.value, ancestors)
29
+ );
30
+ });
31
+ }
32
+ const prototype = Object.getPrototypeOf(value);
33
+ if (prototype !== Object.prototype && prototype !== null) return false;
34
+ return Reflect.ownKeys(value).every((key) => {
35
+ if (typeof key !== "string") return false;
36
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
37
+ return (
38
+ descriptor?.enumerable === true &&
39
+ "value" in descriptor &&
40
+ isJsonValueAt(descriptor.value, ancestors)
41
+ );
42
+ });
43
+ } catch {
44
+ return false;
45
+ } finally {
46
+ ancestors.delete(value);
47
+ }
48
+ }
49
+
50
+ function isArrayIndex(key: PropertyKey): boolean {
51
+ if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key)) return false;
52
+ const index = Number(key);
53
+ return Number.isSafeInteger(index) && index >= 0 && index < 4_294_967_295;
54
+ }
55
+
56
+ /**
57
+ * Any authoring library may supply a JSON Schema object; the engine checks its supported subset.
58
+ * That is why `object` is as narrow as the first arm gets: an unbranded schema from an unknown
59
+ * library has to be accepted here and rejected by `parseJsonSchema`, so only a branded one is
60
+ * checked against `Output`.
61
+ *
62
+ * The second arm looks subsumed by the first and is not: excess-property checking applies to an
63
+ * object literal written at this type, so without an index signature to land in, a schema written
64
+ * inline — `{ type: "object", properties: … }` — is rejected for every keyword it names.
65
+ */
66
+ export type OutputSchema<Output extends JsonValue = JsonValue> =
67
+ | (object & {
68
+ /** Type-only carrier for structurally compatible schema libraries. */
69
+ readonly "~output"?: Output;
70
+ })
71
+ | {
72
+ readonly [keyword: string]: unknown;
73
+ readonly "~output"?: Output;
74
+ };
@@ -0,0 +1,31 @@
1
+ import type { ParticipantRef } from "./participants";
2
+
3
+ /**
4
+ * A route: `from` may message `to`, which answers over it when asked; `between` is both directions,
5
+ * sharing one count. Its purpose is shown with every message on it.
6
+ */
7
+ export type MessageAccess =
8
+ | {
9
+ from: ParticipantRef;
10
+ to: ParticipantRef;
11
+ between?: never;
12
+ purpose: string;
13
+ /** Lowers the engine's ceiling of messages on this route an attempt, answers included. */
14
+ maxMessages?: number;
15
+ }
16
+ | {
17
+ between: readonly [ParticipantRef, ParticipantRef];
18
+ from?: never;
19
+ to?: never;
20
+ purpose: string;
21
+ /** Lowers the engine's ceiling of messages on this route an attempt, answers included. */
22
+ maxMessages?: number;
23
+ };
24
+
25
+ export interface Messaging {
26
+ /**
27
+ * Grants routes in the current scope atomically: a stage's close when it ends, the rest with the
28
+ * attempt. A pair has one open route at a time; granting it again must match, or it rejects.
29
+ */
30
+ allow(access: MessageAccess): Promise<void>;
31
+ }
@@ -0,0 +1,114 @@
1
+ import type { AgentRef, JsonValue, OutputSchema, ParticipantRef, WorkflowContext } from "./index";
2
+ import { isAnswered } from "./index";
3
+
4
+ // The sketches in docs/design/messaging.md's "Routes by shape", as a workflow writes them.
5
+ declare const workflow: WorkflowContext;
6
+ declare const implementer: AgentRef;
7
+ declare const reviewer: AgentRef;
8
+ declare const architect: AgentRef;
9
+ declare const planner: AgentRef;
10
+ declare const critic: AgentRef;
11
+ declare const tester: AgentRef;
12
+ declare const runtime: string;
13
+ declare const LENSES: readonly { key: string }[];
14
+ declare const FIX_REPORT: OutputSchema<{ fixed: string[] }>;
15
+ declare const FINDINGS: OutputSchema<{ findings: number }>;
16
+ declare const PLAN: OutputSchema<{ steps: string[] }>;
17
+ declare function lensPrompt(key: string): string;
18
+ declare function planPrompt(ticket: string): string;
19
+ declare const ticket: string;
20
+
21
+ async function bothWays(): Promise<void> {
22
+ await workflow.messages.allow({
23
+ between: [implementer, reviewer],
24
+ purpose: "Review and revise the implementation until both agree it is ready.",
25
+ });
26
+ }
27
+
28
+ async function oneWayNothingOwed(): Promise<JsonValue> {
29
+ const fixer = await workflow.agents.open({ key: "fixer", runtime });
30
+ const lenses = await workflow.parallel(LENSES, (lens) =>
31
+ workflow.agents.open({ key: lens.key, runtime }),
32
+ );
33
+ for (const lens of lenses) {
34
+ await workflow.messages.allow({
35
+ from: lens,
36
+ to: fixer,
37
+ purpose: "Report each finding as you confirm it, then `done`; the fixer does not reply.",
38
+ });
39
+ }
40
+ const [fixed] = await Promise.all([
41
+ fixer.run({
42
+ prompt:
43
+ "Fix each finding the lenses send; when every lens has said done, report what changed.",
44
+ schema: FIX_REPORT,
45
+ }),
46
+ workflow.parallel(lenses, (lens) =>
47
+ lens.run({ prompt: lensPrompt(lens.key), schema: FINDINGS }),
48
+ ),
49
+ ]);
50
+ return isAnswered(fixed.outcome) ? fixed.outcome.value : null;
51
+ }
52
+
53
+ async function oneWayWithAnAnswer(): Promise<void> {
54
+ await workflow.messages.allow({
55
+ from: implementer,
56
+ to: architect,
57
+ purpose: "Ask about the design when the ticket document does not settle it.",
58
+ });
59
+ }
60
+
61
+ async function room(): Promise<void> {
62
+ const members = [planner, critic, tester];
63
+ for (const [i, a] of members.entries()) {
64
+ for (const b of members.slice(i + 1)) {
65
+ await workflow.messages.allow({
66
+ between: [a, b],
67
+ purpose: "Agree the plan in PLAN.md; ask when an entry there is unclear.",
68
+ });
69
+ }
70
+ }
71
+ }
72
+
73
+ async function lowerLimit(): Promise<void> {
74
+ await workflow.messages.allow({
75
+ between: [implementer, reviewer],
76
+ purpose: "Review and revise the implementation until both agree it is ready.",
77
+ maxMessages: 10,
78
+ });
79
+ }
80
+
81
+ async function routeForOneStage(): Promise<{ steps: string[] }> {
82
+ const plan = await workflow.stage("plan", { result: PLAN }, async () => {
83
+ await workflow.messages.allow({
84
+ from: planner,
85
+ to: architect,
86
+ purpose: "Ask about the design while drafting the plan.",
87
+ });
88
+ const { outcome } = await planner.run({ prompt: planPrompt(ticket), schema: PLAN });
89
+ return isAnswered(outcome) ? outcome.value : workflow.stop(outcome.reason);
90
+ });
91
+ await workflow.agents.stop("architect", "design questions are settled");
92
+ return plan;
93
+ }
94
+
95
+ async function refused(): Promise<void> {
96
+ // A workflow names only participants the engine issued it.
97
+ // @ts-expect-error A `{ key }` literal is not a ParticipantRef.
98
+ const forged: ParticipantRef = { key: "reviewer" };
99
+ await workflow.messages.allow({
100
+ // @ts-expect-error Nor is one inline.
101
+ from: { key: "reviewer" },
102
+ to: implementer,
103
+ purpose: "forged",
104
+ });
105
+ await workflow.messages.allow({
106
+ // @ts-expect-error A route is one shape or the other.
107
+ between: [implementer, reviewer],
108
+ from: implementer,
109
+ purpose: "both shapes",
110
+ });
111
+ void forged;
112
+ }
113
+
114
+ void [bothWays, oneWayNothingOwed, oneWayWithAnAnswer, room, lowerLimit, routeForOneStage, refused];
@@ -0,0 +1,33 @@
1
+ import type { PARTICIPANT } from "../participant";
2
+ import type { JsonObject } from "./json";
3
+
4
+ export type ParticipantKey = string;
5
+
6
+ /**
7
+ * A participant the engine issued: an agent this workflow opened, or later a connected outside
8
+ * session. Only the engine makes one, so a workflow cannot name a participant it was not given.
9
+ */
10
+ export interface ParticipantRef {
11
+ /** Logical key in the workflow context that issued this reference. */
12
+ readonly key: ParticipantKey;
13
+ readonly [PARTICIPANT]: true;
14
+ }
15
+
16
+ export interface ExternalParticipantSpec {
17
+ key: ParticipantKey;
18
+ /** Defaults to the workflow's discoverable session binding. */
19
+ bindingPath?: string;
20
+ labels?: JsonObject;
21
+ }
22
+
23
+ export interface ExternalParticipantRef extends ParticipantRef {
24
+ /** Stops routing and fails outstanding response obligations involving this participant. */
25
+ release(reason?: string): Promise<boolean>;
26
+ }
27
+
28
+ export interface ParticipantDirectory {
29
+ /** Connects a session the engine did not start; an occupied participant key rejects. */
30
+ connect(spec: ExternalParticipantSpec): Promise<ExternalParticipantRef>;
31
+ /** Finds an agent or connected participant visible in this workflow scope. */
32
+ get(key: ParticipantKey): Promise<ParticipantRef | null>;
33
+ }