agentswf 0.1.0-beta.3 → 0.1.0-beta.5
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.
- package/package.json +4 -4
- package/src/testing.ts +1 -0
- package/src/testing.typecheck.ts +163 -0
- package/src/workflow.ts +2 -1
- package/src/workflow.typecheck.ts +309 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentswf",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.5",
|
|
4
4
|
"description": "agents.wf: an engine that runs workflows made of coding agents. Gives you awf, agentswf/workflow and agentswf/testing",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://agents.wf",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/romanlv/agentswf.git",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@agentswf/contract": "0.1.0-beta.
|
|
29
|
-
"@agentswf/engine": "0.1.0-beta.
|
|
28
|
+
"@agentswf/contract": "0.1.0-beta.5",
|
|
29
|
+
"@agentswf/engine": "0.1.0-beta.5"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
package/src/testing.ts
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Checked by `tsc --noEmit`, never run: what `agentswf/testing` offers a workflow's test, and what
|
|
2
|
+
// it doesn't. Each deprecated name compiles until story 039's last task removes it.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
answer,
|
|
6
|
+
noAnswer,
|
|
7
|
+
reply,
|
|
8
|
+
type TestOptions,
|
|
9
|
+
type TestRun,
|
|
10
|
+
type TestTurn,
|
|
11
|
+
type TestTurnOutcome,
|
|
12
|
+
testWorkflow,
|
|
13
|
+
} from "./testing";
|
|
14
|
+
import { AgentEnded, defineWorkflow, type OutputSchema, ParticipantReply } from "./workflow";
|
|
15
|
+
|
|
16
|
+
type Surface = typeof import("./testing");
|
|
17
|
+
// @ts-expect-error Only a claude TUI agent may wait, which its harness decides, not a workflow.
|
|
18
|
+
export type Waiting = Surface["waiting"];
|
|
19
|
+
// @ts-expect-error The engine's own tests, which say where an agent may await a reply, call it.
|
|
20
|
+
export type RunTest = Surface["runTest"];
|
|
21
|
+
|
|
22
|
+
const plain = defineWorkflow({
|
|
23
|
+
meta: { name: "plain", description: "One agent." },
|
|
24
|
+
run: async (wf) => {
|
|
25
|
+
const agent = await wf.agents.open({ key: "a", runtime: "claude" });
|
|
26
|
+
return (await agent.run({ prompt: "Go." })).outcome.kind;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const greet = defineWorkflow<{ name: string }, string>({
|
|
30
|
+
meta: { name: "greet", description: "Greets one name." },
|
|
31
|
+
prepare: ({ argv }) => ({ name: argv[0] ?? "nobody" }),
|
|
32
|
+
run: async (_wf, { name }) => `hi ${name}`,
|
|
33
|
+
});
|
|
34
|
+
const Ping = { type: "object" } as OutputSchema<{ n: number }>;
|
|
35
|
+
const withEvents = defineWorkflow({
|
|
36
|
+
meta: { name: "events", description: "Woken by pings." },
|
|
37
|
+
file: import.meta.path,
|
|
38
|
+
events: { ping: Ping },
|
|
39
|
+
run: async (wf) => (await wf.receive()).length,
|
|
40
|
+
});
|
|
41
|
+
const bare = { meta: plain.meta, run: plain.run };
|
|
42
|
+
const maybe = defineWorkflow<{ x: number } | null, number>({
|
|
43
|
+
meta: { name: "maybe", description: "Args or none." },
|
|
44
|
+
prepare: () => null,
|
|
45
|
+
run: async (_wf, args) => args?.x ?? 0,
|
|
46
|
+
});
|
|
47
|
+
const listed = defineWorkflow<string[], number>({
|
|
48
|
+
meta: { name: "listed", description: "Counts." },
|
|
49
|
+
prepare: ({ argv }) => [...argv],
|
|
50
|
+
run: async (_wf, names) => names.length,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export async function argsShapes(): Promise<void> {
|
|
54
|
+
// Null among its args: they may be left out.
|
|
55
|
+
await testWorkflow(maybe);
|
|
56
|
+
await testWorkflow(maybe, { args: { x: 1 } });
|
|
57
|
+
await testWorkflow(listed, { args: ["a"] });
|
|
58
|
+
// @ts-expect-error Array args are required too.
|
|
59
|
+
await testWorkflow(listed, {});
|
|
60
|
+
// Deprecated: array args given second, with no options.
|
|
61
|
+
await testWorkflow(listed, ["a", "b"]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function oneSignature(): Promise<void> {
|
|
65
|
+
await testWorkflow(plain);
|
|
66
|
+
await testWorkflow(plain, { agents: { a: answer("done") }, maxTurns: 3, id: "r-1" });
|
|
67
|
+
await testWorkflow(greet, { args: { name: "ada" } });
|
|
68
|
+
// @ts-expect-error A workflow that takes args is given them.
|
|
69
|
+
await testWorkflow(greet);
|
|
70
|
+
// @ts-expect-error A workflow that takes args is given them, among its options.
|
|
71
|
+
await testWorkflow(greet, { agents: { a: answer("done") } });
|
|
72
|
+
// @ts-expect-error Its args given second are not options: an object there always is.
|
|
73
|
+
await testWorkflow(greet, { name: "ada" });
|
|
74
|
+
// @ts-expect-error A misspelled option is refused.
|
|
75
|
+
await testWorkflow(plain, { agnets: { a: answer("done") } });
|
|
76
|
+
// @ts-expect-error A workflow without events is sent none.
|
|
77
|
+
await testWorkflow(plain, { event: { name: "ping", data: { n: 1 } } });
|
|
78
|
+
// @ts-expect-error The one form tests what defineWorkflow made.
|
|
79
|
+
await testWorkflow(bare, {});
|
|
80
|
+
await testWorkflow(withEvents, {
|
|
81
|
+
earlier: { stages: { plan: null }, mailbox: [] },
|
|
82
|
+
event: { name: "ping", data: { n: 1 } },
|
|
83
|
+
fromStage: "plan",
|
|
84
|
+
});
|
|
85
|
+
const shared: TestOptions = { agents: { a: answer("done") } };
|
|
86
|
+
await testWorkflow(plain, shared);
|
|
87
|
+
await testWorkflow(greet, { ...shared, args: { name: "ada" } });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function removed(): Promise<void> {
|
|
91
|
+
// @ts-expect-error A wait is the harness's check-in, not a script's ending.
|
|
92
|
+
reply.waiting("for CI");
|
|
93
|
+
// @ts-expect-error Where an agent may await a reply is the engine's list.
|
|
94
|
+
await testWorkflow(plain, { awaitReply: "anywhere" });
|
|
95
|
+
// @ts-expect-error A message script ends a turn; it answers no call.
|
|
96
|
+
await testWorkflow(plain, { messages: { a: () => answer("done") } });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function deprecated(run: TestRun<string>): Promise<void> {
|
|
100
|
+
await testWorkflow(plain, null, {
|
|
101
|
+
agents: { a: [reply.silent(), reply.blocked(), reply.failed(), reply.timedOut()] },
|
|
102
|
+
compactions: { a: [reply.hang(), reply.interrupted(), reply.needsLogin()] },
|
|
103
|
+
messages: { a: [(message) => message.silent(), (message) => message.fail("crashed")] },
|
|
104
|
+
recorded: { plan: null },
|
|
105
|
+
});
|
|
106
|
+
await testWorkflow(greet, { name: "ada" }, undefined);
|
|
107
|
+
await testWorkflow(plain, null);
|
|
108
|
+
await testWorkflow(bare, null, {});
|
|
109
|
+
await testWorkflow(withEvents, {
|
|
110
|
+
id: "k",
|
|
111
|
+
event: { name: "ping", data: { n: 1 }, id: "e-1" },
|
|
112
|
+
events: [{ during: "a#1", name: "ping", data: { n: 2 }, id: "e-2" }],
|
|
113
|
+
mailbox: [],
|
|
114
|
+
});
|
|
115
|
+
void run.stopped?.reason;
|
|
116
|
+
void run.parked?.label;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function current(): Promise<void> {
|
|
120
|
+
const run = await testWorkflow(plain, {
|
|
121
|
+
agents: { a: [noAnswer.silent(), noAnswer.failed("crashed"), answer("done")] },
|
|
122
|
+
messages: {
|
|
123
|
+
a: [() => {}, async (message) => message.reply("Done."), () => noAnswer.failed("crashed")],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const turn: TestTurn = run.turns[0]!;
|
|
127
|
+
const outcome: TestTurnOutcome = turn.outcome;
|
|
128
|
+
// @ts-expect-error A turn records the engine's word, unanswered, not the script's.
|
|
129
|
+
const silent: TestTurnOutcome = "silent";
|
|
130
|
+
void [outcome, silent];
|
|
131
|
+
if (run.ending.kind === "stopped") void [run.ending.stage, run.ending.reason];
|
|
132
|
+
if (run.ending.kind === "completed") void run.ending.value;
|
|
133
|
+
await testWorkflow(withEvents, {
|
|
134
|
+
id: "k",
|
|
135
|
+
event: { name: "ping", data: { n: 1 }, eventId: "e-1" },
|
|
136
|
+
events: [{ during: "a#1", name: "ping", data: { n: 2 }, eventId: "e-2" }],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const declaring = defineWorkflow({
|
|
141
|
+
meta: { name: "declaring", description: "Declares its thread's replies and an agent's end." },
|
|
142
|
+
file: import.meta.path,
|
|
143
|
+
events: { ping: Ping, "thread:reply": ParticipantReply, "a:ended": AgentEnded },
|
|
144
|
+
run: async (wf) => (await wf.receive()).length,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export async function scriptedReplies(): Promise<void> {
|
|
148
|
+
await testWorkflow(declaring, {
|
|
149
|
+
events: [{ during: "a#1", name: "thread:reply", data: { text: "hi", author: "dana" } }],
|
|
150
|
+
});
|
|
151
|
+
// @ts-expect-error A reply's data is ParticipantReply's: text, and author.
|
|
152
|
+
await testWorkflow(declaring, {
|
|
153
|
+
events: [{ during: "a#1", name: "thread:reply", data: { body: "hi" } }],
|
|
154
|
+
});
|
|
155
|
+
// @ts-expect-error Only awf sends an agent's end.
|
|
156
|
+
await testWorkflow(declaring, {
|
|
157
|
+
events: [{ during: "a#1", name: "a:ended", data: { turn: "t", meta: null } }],
|
|
158
|
+
});
|
|
159
|
+
// Deprecated: the old name, from the participant's key.
|
|
160
|
+
await testWorkflow(withEvents, {
|
|
161
|
+
events: [{ during: "a#1", name: "participant:reply", from: "thread", data: { text: "hi" } }],
|
|
162
|
+
});
|
|
163
|
+
}
|
package/src/workflow.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
// The list lives in the contract, which the engine also serves it from to the workflows it loads.
|
|
2
|
+
export * from "@agentswf/contract/author";
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Checked by `tsc --noEmit`, never run: what `agentswf/workflow` offers an author, and what it
|
|
2
|
+
// doesn't. Each deprecated name compiles until story 039's last task removes it.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type AgentDirectory,
|
|
6
|
+
AgentEnded,
|
|
7
|
+
type AgentEndedEvent,
|
|
8
|
+
type AgentOpenSpec,
|
|
9
|
+
type AgentRef,
|
|
10
|
+
type CallerRef,
|
|
11
|
+
type CodeParticipantRef,
|
|
12
|
+
type CodeParticipantSpec,
|
|
13
|
+
type Duration,
|
|
14
|
+
defineExecutableWorkflow,
|
|
15
|
+
defineWorkflow,
|
|
16
|
+
duration,
|
|
17
|
+
type EventAgentDirectory,
|
|
18
|
+
type EventAgentOpenSpec,
|
|
19
|
+
type EventAgentRef,
|
|
20
|
+
type EventWorkflowContext,
|
|
21
|
+
isAnswered,
|
|
22
|
+
type JsonValue,
|
|
23
|
+
type OutputSchema,
|
|
24
|
+
type ParticipantRef,
|
|
25
|
+
ParticipantReply,
|
|
26
|
+
type ParticipantReplyEvent,
|
|
27
|
+
tool,
|
|
28
|
+
type WorkflowContext,
|
|
29
|
+
type WorkflowEvent,
|
|
30
|
+
yesNo,
|
|
31
|
+
} from "./workflow";
|
|
32
|
+
|
|
33
|
+
type Surface = typeof import("./workflow");
|
|
34
|
+
// @ts-expect-error The engine's own check of a workflow's events, not an author's.
|
|
35
|
+
export type EventWorkflowProblem = Surface["eventWorkflowProblem"];
|
|
36
|
+
// @ts-expect-error The engine reads an agent's placement; an author writes it.
|
|
37
|
+
export type PlacementOf = Surface["placementOf"];
|
|
38
|
+
// @ts-expect-error Removed: a workflow is a `Workflow`, from `defineWorkflow`.
|
|
39
|
+
export type Definition = import("./workflow").WorkflowDefinition<null, null>;
|
|
40
|
+
// @ts-expect-error Removed with `wf.call`, which was never built.
|
|
41
|
+
export type CallSpec = import("./workflow").WorkflowCallSpec<null, null>;
|
|
42
|
+
// @ts-expect-error Removed with `wf.steps`, which was never built.
|
|
43
|
+
export type StepSpec = import("./workflow").StepSpec;
|
|
44
|
+
// @ts-expect-error One `WaitOptions` for receive, race and select.
|
|
45
|
+
export type SelectOptions = import("./workflow").SelectOptions;
|
|
46
|
+
// @ts-expect-error One `WaitOptions` for receive, race and select.
|
|
47
|
+
export type ReceiveOptions = import("./workflow").ReceiveOptions;
|
|
48
|
+
|
|
49
|
+
declare const plain: WorkflowContext;
|
|
50
|
+
declare const Changed: OutputSchema<{ head: string }>;
|
|
51
|
+
type Events = { changed: typeof Changed };
|
|
52
|
+
declare const wf: WorkflowContext<Events>;
|
|
53
|
+
|
|
54
|
+
export async function removed(): Promise<void> {
|
|
55
|
+
// @ts-expect-error A stage and wf.sleep do what steps would have.
|
|
56
|
+
await plain.steps.run({ id: "x" }, async () => null);
|
|
57
|
+
// @ts-expect-error One workflow calling another was never built.
|
|
58
|
+
await plain.call({ id: "child" });
|
|
59
|
+
// @ts-expect-error Opening the same key again returns the agent opened before.
|
|
60
|
+
await plain.agents.attach("reviewer");
|
|
61
|
+
// @ts-expect-error A participant's ref is what connect returned.
|
|
62
|
+
await plain.participants.get("coordinator");
|
|
63
|
+
// @ts-expect-error A loop over each timer is a select with each as a branch.
|
|
64
|
+
wf.timers();
|
|
65
|
+
// @ts-expect-error receive takes every event not yet acknowledged; select takes one at a time.
|
|
66
|
+
await wf.receive({ take: "oldest" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function deprecated(): Promise<void> {
|
|
70
|
+
const reviewer = await wf.agents.open({ key: "r", runtime: "claude", labels: { lens: "x" } });
|
|
71
|
+
await reviewer.fork({ key: "r2", labels: { lens: "y" } });
|
|
72
|
+
await plain.agents.forkCaller({ key: "me2", labels: { lens: "z" } });
|
|
73
|
+
const { outcome } = await reviewer.run({ id: "turn-1", prompt: "Review." });
|
|
74
|
+
if (isAnswered(outcome)) void outcome.value;
|
|
75
|
+
await reviewer.compact({ id: "c-1", prompt: "Keep the findings." });
|
|
76
|
+
await reviewer.set({ id: "s-1", effort: "high" });
|
|
77
|
+
const batch = await wf.stage("first", () => wf.receive());
|
|
78
|
+
void batch;
|
|
79
|
+
const turn = await reviewer.start({ id: "turn-2", prompt: "Again." });
|
|
80
|
+
const won = await wf.race("review", { done: turn, quiet: "1d" });
|
|
81
|
+
if (won.done && turn.is({ name: "agent:ended", from: "r", data: won.done })) void turn.id;
|
|
82
|
+
for await (const next of wf.select({ done: turn })) void next.done;
|
|
83
|
+
// One context and one agent: the event-only names are the same types.
|
|
84
|
+
const context: EventWorkflowContext<Events> = wf;
|
|
85
|
+
const agent: EventAgentRef = reviewer;
|
|
86
|
+
const spec: EventAgentOpenSpec = { key: "k", runtime: "claude", keepAlive: {} };
|
|
87
|
+
const agents: EventAgentDirectory = context.agents;
|
|
88
|
+
void [agent satisfies AgentRef, spec satisfies AgentOpenSpec, agents satisfies AgentDirectory];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function deprecatedAgents(): Promise<void> {
|
|
92
|
+
const worker = await plain.agents.open({ key: "worker", runtime: "codex" });
|
|
93
|
+
await worker.fork({ key: "w2", placement: "headless", metered: true, effort: "low" });
|
|
94
|
+
await plain.agents.forkCaller({ key: "me2", placement: "headless" });
|
|
95
|
+
await plain.agents.stop("worker", "done");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function caller(): Promise<void> {
|
|
99
|
+
const me = await plain.agents.caller({ key: "me" });
|
|
100
|
+
if (!me) return;
|
|
101
|
+
// @ts-expect-error The calling session's model and effort are the operator's.
|
|
102
|
+
await me.set({ effort: "high" });
|
|
103
|
+
// @ts-expect-error The calling session is the operator's, and is not compacted.
|
|
104
|
+
await me.compact({ prompt: "Keep the plan." });
|
|
105
|
+
// @ts-expect-error A fork of the calling session takes no tools.
|
|
106
|
+
await me.fork({ key: "me3", tools: {} });
|
|
107
|
+
// @ts-expect-error A fork continues its parent's session on its harness and model.
|
|
108
|
+
await me.fork({ key: "me4", runtime: { model: "gpt-6-luna" } });
|
|
109
|
+
// @ts-expect-error The caller is not an agent the run opened.
|
|
110
|
+
const opened: AgentRef = me;
|
|
111
|
+
void opened;
|
|
112
|
+
const fork = await me.fork({ key: "me5", runtime: { placement: "headless", metered: true } });
|
|
113
|
+
const ref: CallerRef = fork;
|
|
114
|
+
await wf.messages.allow({ between: [me, fork], purpose: "Agree on the plan." });
|
|
115
|
+
const won = await wf.race("either", { mine: me, theirs: ref, quiet: "1h" });
|
|
116
|
+
if (won.mine && me.is({ name: "agent:ended", from: "me", data: won.mine })) void won.mine;
|
|
117
|
+
await fork.close("enough");
|
|
118
|
+
await me.close();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function time(): Promise<void> {
|
|
122
|
+
const reviewer = await wf.agents.open({ key: "r", runtime: "claude", timeout: "90s" });
|
|
123
|
+
const until = new Date(Date.now() + 60_000);
|
|
124
|
+
await reviewer.run({ prompt: "Review.", timeout: "1.5h" });
|
|
125
|
+
await reviewer.run({ prompt: "Review.", timeout: until });
|
|
126
|
+
await reviewer.run({ prompt: "Review.", timeout: false });
|
|
127
|
+
await reviewer.start({ prompt: "Review.", timeout: "10m" });
|
|
128
|
+
await reviewer.compact({ prompt: "Keep it.", timeout: "2m" });
|
|
129
|
+
await reviewer.set({ effort: "high", timeout: "30s" });
|
|
130
|
+
await reviewer.fork({ key: "r2", timeout: "1m" });
|
|
131
|
+
await plain.agents.forkCaller({ key: "me6", timeout: until });
|
|
132
|
+
await plain.parallel([1], async (n) => n, { timeout: "20m" });
|
|
133
|
+
await plain.decisions.decide({
|
|
134
|
+
key: "ok",
|
|
135
|
+
model: "jev",
|
|
136
|
+
state: "x",
|
|
137
|
+
questions: { ok: yesNo("Is it ok?") },
|
|
138
|
+
timeout: "30s",
|
|
139
|
+
});
|
|
140
|
+
await wf.agents.open({
|
|
141
|
+
key: "k",
|
|
142
|
+
runtime: "claude",
|
|
143
|
+
keepAlive: { window: "90s" },
|
|
144
|
+
tools: { look: tool({ description: "Look.", handler: () => "seen", timeout: "30s" }) },
|
|
145
|
+
});
|
|
146
|
+
await wf.messages.allow({ from: reviewer, to: reviewer, purpose: "p", timeout: "30m" });
|
|
147
|
+
wf.timer("in-90s", "90s");
|
|
148
|
+
wf.timer("in-1.5h", "1.5h");
|
|
149
|
+
wf.timer("at", until);
|
|
150
|
+
await wf.sleep("a-while", "14d");
|
|
151
|
+
const every: Duration = duration(String(JSON.parse('"2h"')));
|
|
152
|
+
void every;
|
|
153
|
+
const end: Date | undefined = plain.deadline;
|
|
154
|
+
void end;
|
|
155
|
+
// @ts-expect-error A duration is a decimal and s, m, h or d.
|
|
156
|
+
await reviewer.run({ prompt: "Review.", timeout: "10x" });
|
|
157
|
+
// @ts-expect-error Nor a number: milliseconds are what timeoutMs took.
|
|
158
|
+
await reviewer.run({ prompt: "Review.", timeout: 60_000 });
|
|
159
|
+
// @ts-expect-error A timer's duration is the same grammar.
|
|
160
|
+
wf.timer("typo", "10x");
|
|
161
|
+
// @ts-expect-error A route's timeout is a length of time per question, not a time.
|
|
162
|
+
await wf.messages.allow({ from: reviewer, to: reviewer, purpose: "p", timeout: until });
|
|
163
|
+
// @ts-expect-error A tool's timeout is each call's budget, not a time.
|
|
164
|
+
tool({ description: "Look.", handler: () => "seen", timeout: until });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function deprecatedTime(): Promise<void> {
|
|
168
|
+
const deadline = { unixMilliseconds: Date.now() + 60_000 };
|
|
169
|
+
const reviewer = await wf.agents.open({ key: "r", runtime: "claude", deadline });
|
|
170
|
+
await reviewer.run({ prompt: "Review.", timeoutMs: 60_000, nudge: { deadline } });
|
|
171
|
+
await reviewer.run({ prompt: "Review.", deadline });
|
|
172
|
+
await reviewer.compact({ prompt: "Keep it.", timeoutMs: 60_000 });
|
|
173
|
+
await reviewer.set({ effort: "high", deadline });
|
|
174
|
+
await plain.parallel([1], async (n) => n, { deadline });
|
|
175
|
+
await plain.decisions.decide({
|
|
176
|
+
key: "ok",
|
|
177
|
+
model: "jev",
|
|
178
|
+
state: "x",
|
|
179
|
+
questions: { ok: yesNo("Is it ok?") },
|
|
180
|
+
deadline,
|
|
181
|
+
});
|
|
182
|
+
await wf.agents.open({
|
|
183
|
+
key: "k",
|
|
184
|
+
runtime: "claude",
|
|
185
|
+
keepAlive: { timeoutMs: 90_000 },
|
|
186
|
+
tools: { look: tool({ description: "Look.", handler: () => "seen", timeoutMs: false }) },
|
|
187
|
+
});
|
|
188
|
+
await wf.messages.allow({ from: reviewer, to: reviewer, purpose: "p", timeoutMs: 60_000 });
|
|
189
|
+
const left = plain.deadline ? plain.deadline.unixMilliseconds - Date.now() : undefined;
|
|
190
|
+
void left;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const old = defineExecutableWorkflow({
|
|
194
|
+
definition: {
|
|
195
|
+
meta: { name: "old", description: "beta.2's shape.", whenToUse: "Never." },
|
|
196
|
+
run: async () => null,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
export async function current(): Promise<void> {
|
|
201
|
+
const fixer = await wf.agents.open({ key: "fixer", runtime: "claude" });
|
|
202
|
+
await fixer.start({ prompt: "Fix it." });
|
|
203
|
+
const first = await wf.race(
|
|
204
|
+
"fix-or-quiet",
|
|
205
|
+
{ fixed: fixer, quiet: "1d" },
|
|
206
|
+
{ label: () => `fixing at ${wf.now().toISOString()}` },
|
|
207
|
+
);
|
|
208
|
+
if (first.quiet) await fixer.turn?.cancel("quiet for a day");
|
|
209
|
+
if (first.fixed?.outcome.answered) void first.fixed.outcome.value;
|
|
210
|
+
await wf.receive({ label: () => "waiting" });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default defineWorkflow({
|
|
214
|
+
meta: { name: "current", description: "The new forms." },
|
|
215
|
+
run: async () => null,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Story 039 task 7: participants and agents routed to the code are declared in `events`.
|
|
219
|
+
const Change: OutputSchema<{ id: string }> = {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: { id: { type: "string" } },
|
|
222
|
+
required: ["id"],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const declaring = defineWorkflow({
|
|
226
|
+
meta: { name: "declaring", description: "Declares what wakes it." },
|
|
227
|
+
events: { change: Change, "thread:reply": ParticipantReply, "fixer:ended": AgentEnded },
|
|
228
|
+
async run(wf) {
|
|
229
|
+
const thread: ParticipantRef = await wf.participants.connect({ key: "thread" });
|
|
230
|
+
await thread.post("Looking.");
|
|
231
|
+
// @ts-expect-error A ParticipantRef is not a source: race the signal of its declared reply.
|
|
232
|
+
await wf.race("bare", { reply: thread });
|
|
233
|
+
// @ts-expect-error is(e) stays on timers and agents: a reply is told by its name.
|
|
234
|
+
void thread.is;
|
|
235
|
+
const fixer = await wf.agents.open({ key: "fixer", runtime: "claude" });
|
|
236
|
+
await fixer.start({ prompt: "Fix it." });
|
|
237
|
+
for (const e of await wf.receive()) {
|
|
238
|
+
if (e.name === "thread:reply") {
|
|
239
|
+
const reply: ParticipantReply = e.data;
|
|
240
|
+
void reply.author;
|
|
241
|
+
}
|
|
242
|
+
if (e.name === "fixer:ended") void e.data.outcome.answered;
|
|
243
|
+
// @ts-expect-error A misspelled end is no event the workflow declares.
|
|
244
|
+
if (e.name === "fxier:ended") void e;
|
|
245
|
+
if (fixer.is(e)) void e.data.meta;
|
|
246
|
+
// Narrowed under the name it arrives by, the declared one included.
|
|
247
|
+
if (fixer.is(e) && e.name === "fixer:ended") void e.data.outcome;
|
|
248
|
+
}
|
|
249
|
+
const next = await wf.race("approval", { reply: wf.signal("thread:reply"), quiet: "1d" });
|
|
250
|
+
// @ts-expect-error An agent's end is waited on by naming the agent: { done: fixer }.
|
|
251
|
+
wf.signal("fixer:ended");
|
|
252
|
+
if (next.reply) void next.reply.data.text;
|
|
253
|
+
// @ts-expect-error A run speaks for no participant: wf.emit sends its target's own events.
|
|
254
|
+
await wf.emit(declaringTarget, "k", "thread:reply", { text: "hi" });
|
|
255
|
+
return null;
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
declare const declaringTarget: typeof declaring;
|
|
259
|
+
|
|
260
|
+
// @ts-expect-error `{key}:reply` takes ParticipantReply, and no other schema.
|
|
261
|
+
export const wrongSchema = defineWorkflow({
|
|
262
|
+
meta: { name: "wrong", description: "An awf name with a schema of its own." },
|
|
263
|
+
events: { "thread:reply": Change },
|
|
264
|
+
run: async () => null,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// @ts-expect-error Only `{key}:reply` and `{key}:ended` have a colon.
|
|
268
|
+
export const unknownColon = defineWorkflow({
|
|
269
|
+
meta: { name: "colon", description: "A colon name awf doesn't make." },
|
|
270
|
+
events: { "thread:said": ParticipantReply },
|
|
271
|
+
run: async () => null,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
export const awfStart = defineWorkflow({
|
|
275
|
+
meta: { name: "start", description: "A reply can't start a run." },
|
|
276
|
+
file: "/flows/start.ts",
|
|
277
|
+
events: { change: Change, "thread:reply": ParticipantReply },
|
|
278
|
+
// @ts-expect-error `starts` takes only the workflow's own events.
|
|
279
|
+
starts: ["thread:reply"],
|
|
280
|
+
run: async () => null,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export const stillCompiles = defineWorkflow({
|
|
284
|
+
meta: { name: "old-routing", description: "The deprecated form." },
|
|
285
|
+
events: { change: Change },
|
|
286
|
+
async run(wf) {
|
|
287
|
+
const thread: CodeParticipantRef = await wf.participants.connect({ key: "thread", to: "code" });
|
|
288
|
+
await thread.post("Looking.");
|
|
289
|
+
for (const e of await wf.receive()) if (thread.is(e)) void e.data.text;
|
|
290
|
+
const next = await wf.race("reply", { reply: thread, quiet: "1d" });
|
|
291
|
+
if (next.reply) void next.reply.text;
|
|
292
|
+
// A spec held in a variable, which no excess-property check sets apart, still routes.
|
|
293
|
+
const spec: CodeParticipantSpec = { key: "pager", to: "code" };
|
|
294
|
+
const pager = await wf.participants.connect(spec);
|
|
295
|
+
for (const e of await wf.receive()) if (pager.is(e)) void e.data.text;
|
|
296
|
+
await wf.race("page", { reply: pager, quiet: "1d" });
|
|
297
|
+
return null;
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// As before task 7: an end read from a loosely typed event narrows to `AgentEndedEvent`.
|
|
302
|
+
declare const loose: WorkflowEvent<string, JsonValue>;
|
|
303
|
+
declare function handleEnd(e: AgentEndedEvent<{ n: number }>): void;
|
|
304
|
+
declare function handleReply(e: ParticipantReplyEvent): void;
|
|
305
|
+
export async function looseEvents(inv: AgentRef, thread: CodeParticipantRef): Promise<void> {
|
|
306
|
+
if (inv.is<{ n: number }>(loose)) handleEnd(loose);
|
|
307
|
+
if (inv.is(loose)) void loose.data.outcome;
|
|
308
|
+
if (thread.is(loose)) handleReply(loose);
|
|
309
|
+
}
|