@fungi.computer/shiitake-extensions 0.0.0-stage → 0.0.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -2
  3. package/dist/almanac/contract.d.ts +213 -0
  4. package/dist/almanac/contract.js +77 -0
  5. package/dist/almanac/host.d.ts +3 -0
  6. package/dist/almanac/host.js +171 -0
  7. package/dist/goal/client.d.ts +10 -0
  8. package/dist/goal/client.js +88 -0
  9. package/dist/goal/contract.d.ts +172 -0
  10. package/dist/goal/contract.js +76 -0
  11. package/dist/goal/host.d.ts +5 -0
  12. package/dist/goal/host.js +542 -0
  13. package/dist/home/contract.d.ts +33 -0
  14. package/dist/home/contract.js +25 -0
  15. package/dist/home/host.d.ts +18 -0
  16. package/dist/home/host.js +136 -0
  17. package/dist/home/routing.d.ts +3 -0
  18. package/dist/home/routing.js +80 -0
  19. package/dist/internal/catalog.d.ts +20 -0
  20. package/dist/internal/catalog.js +33 -0
  21. package/dist/internal/cron.d.ts +5 -0
  22. package/dist/internal/cron.js +14 -0
  23. package/dist/internal/history.d.ts +29 -0
  24. package/dist/internal/history.js +41 -0
  25. package/dist/loop/client.d.ts +10 -0
  26. package/dist/loop/client.js +89 -0
  27. package/dist/loop/contract.d.ts +342 -0
  28. package/dist/loop/contract.js +94 -0
  29. package/dist/loop/host.d.ts +4 -0
  30. package/dist/loop/host.js +637 -0
  31. package/dist/plan/client.d.ts +13 -0
  32. package/dist/plan/client.js +51 -0
  33. package/dist/plan/contract.d.ts +105 -0
  34. package/dist/plan/contract.js +38 -0
  35. package/dist/plan/host.d.ts +3 -0
  36. package/dist/plan/host.js +154 -0
  37. package/dist/question/client.d.ts +10 -0
  38. package/dist/question/client.js +28 -0
  39. package/dist/question/contract.d.ts +251 -0
  40. package/dist/question/contract.js +114 -0
  41. package/dist/question/host.d.ts +3 -0
  42. package/dist/question/host.js +997 -0
  43. package/dist/roster.d.ts +9 -0
  44. package/dist/roster.js +80 -0
  45. package/dist/skills/host.d.ts +7 -0
  46. package/dist/skills/host.js +223 -0
  47. package/dist/skills/source.d.ts +28 -0
  48. package/dist/skills/source.js +1 -0
  49. package/dist/subagents/client.d.ts +10 -0
  50. package/dist/subagents/client.js +53 -0
  51. package/dist/subagents/contract.d.ts +54 -0
  52. package/dist/subagents/contract.js +38 -0
  53. package/dist/subagents/host.d.ts +50 -0
  54. package/dist/subagents/host.js +176 -0
  55. package/package.json +117 -3
@@ -0,0 +1,171 @@
1
+ import { defineCollection, defineExtension, ExtensionCommandError, } from "@fungi.computer/shiitake/extensions";
2
+ import * as Clock from "effect/Clock";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Result from "effect/Result";
5
+ import * as Option from "effect/Option";
6
+ import { afterAdmitted, afterRefused, cancelled, occurrenceKey, paused, resumed, settled, } from "@fungi.computer/almanac";
7
+ import { nextOccurrence } from "../internal/cron.js";
8
+ import { almanacContract, Schedule } from "./contract.js";
9
+ /** Hashed policy: every constant Almanac reads comes from here. */
10
+ const config = {
11
+ /** First retry delay after a failed occurrence admission. */
12
+ retryBaseMs: 30_000,
13
+ /** Longest retry delay; one poisoned schedule never blocks the others. */
14
+ retryMaxMs: 3_600_000,
15
+ };
16
+ const collections = {
17
+ schedules: defineCollection(Schedule, {
18
+ key: (schedule) => schedule.id,
19
+ index: (schedule) => schedule.state,
20
+ }),
21
+ };
22
+ function rejected(reason, message) {
23
+ return new ExtensionCommandError({ reason, message });
24
+ }
25
+ function requireSchedule(ctx, id) {
26
+ return ctx.state.schedules.get(id).pipe(Effect.flatMap(Option.match({
27
+ onNone: () => Effect.fail(rejected("not_found", "Almanac schedule does not exist")),
28
+ onSome: Effect.succeed,
29
+ })));
30
+ }
31
+ function sameSchedule(left, right) {
32
+ return (left.sessionId === right.sessionId &&
33
+ left.prompt === right.prompt &&
34
+ left.cron?.expression === right.cron?.expression &&
35
+ left.cron?.timeZone === right.cron?.timeZone &&
36
+ (left.cron !== null || left.nextDueAt === right.nextDueAt));
37
+ }
38
+ /** Create or replay one schedule; its request id is its identity. */
39
+ function create(ctx, schedule) {
40
+ return Effect.gen(function* () {
41
+ const existing = yield* ctx.state.schedules.get(schedule.id);
42
+ if (Option.isSome(existing)) {
43
+ if (sameSchedule(existing.value, schedule))
44
+ return existing.value;
45
+ return yield* Effect.fail(rejected("conflict", "Schedule id is already used by another schedule"));
46
+ }
47
+ yield* ctx.state.schedules.put(schedule);
48
+ yield* ctx.timers.set(schedule.id, schedule.nextDueAt);
49
+ return schedule;
50
+ });
51
+ }
52
+ /**
53
+ * Send the due occurrence through the door. The occurrence request id is
54
+ * `id:occurrenceAt`, so a retried admission replays instead of duplicating.
55
+ * A refused admission is this schedule's own visible state: it records the
56
+ * error and backs off; it never stops Almanac or delays other schedules.
57
+ */
58
+ function fire(ctx, id, now) {
59
+ return Effect.gen(function* () {
60
+ const found = yield* ctx.state.schedules.get(id);
61
+ if (Option.isNone(found) || found.value.state !== "active")
62
+ return;
63
+ const schedule = found.value;
64
+ const admitted = yield* Effect.result(ctx.send({
65
+ sessionId: schedule.sessionId,
66
+ requireExistingSession: schedule.lastCommandId !== null,
67
+ when: "now",
68
+ correlation: schedule.id,
69
+ prompt: {
70
+ text: schedule.prompt,
71
+ requestId: occurrenceKey(schedule),
72
+ delivery: "followUp",
73
+ },
74
+ }));
75
+ if (Result.isFailure(admitted) || admitted.success._tag === "NotReady") {
76
+ const refused = afterRefused(schedule, now, Result.isFailure(admitted)
77
+ ? admitted.failure.message
78
+ : "Session did not accept the occurrence", { baseMs: config.retryBaseMs, maxMs: config.retryMaxMs });
79
+ yield* ctx.state.schedules.put(refused.schedule);
80
+ yield* ctx.timers.set(schedule.id, refused.retryAt);
81
+ return;
82
+ }
83
+ const next = afterAdmitted({ ...schedule, lastCommandId: admitted.success.commandId }, now);
84
+ yield* ctx.state.schedules.put(next);
85
+ if (next.state === "active")
86
+ yield* ctx.timers.set(schedule.id, next.nextDueAt);
87
+ });
88
+ }
89
+ /** Botanical's Almanac on Shiitake timers and the one admission door. */
90
+ export const almanacExtension = defineExtension({
91
+ id: almanacContract.id,
92
+ version: "1.1.0",
93
+ })
94
+ .config(config)
95
+ .state({ version: 1, migrations: {}, collections })
96
+ .define({
97
+ hooks: { onTimer: (ctx, timer) => fire(ctx, timer.key, timer.firedAt) },
98
+ commands: {
99
+ specs: almanacContract.commands,
100
+ handlers: (ctx) => ({
101
+ createOneShot: ({ input }) => create(ctx, {
102
+ id: input.requestId,
103
+ sessionId: input.sessionId,
104
+ prompt: input.prompt,
105
+ cron: null,
106
+ nextDueAt: input.dueAt,
107
+ state: "active",
108
+ lastCommandId: null,
109
+ lastError: null,
110
+ attempt: 0,
111
+ }),
112
+ createRecurring: ({ input }) => Effect.gen(function* () {
113
+ const cron = {
114
+ expression: input.expression,
115
+ timeZone: input.timeZone,
116
+ };
117
+ const now = yield* Clock.currentTimeMillis;
118
+ return yield* create(ctx, {
119
+ id: input.requestId,
120
+ sessionId: input.sessionId,
121
+ prompt: input.prompt,
122
+ cron,
123
+ nextDueAt: yield* nextOccurrence(cron, now),
124
+ state: "active",
125
+ lastCommandId: null,
126
+ lastError: null,
127
+ attempt: 0,
128
+ });
129
+ }),
130
+ read: ({ input }) => ctx.state.schedules
131
+ .get(input.scheduleId)
132
+ .pipe(Effect.map(Option.getOrNull)),
133
+ list: ({ input }) => ctx.state.schedules.page({
134
+ after: input.after,
135
+ limit: input.limit ?? 64,
136
+ }),
137
+ pause: ({ input }) => Effect.gen(function* () {
138
+ const schedule = yield* requireSchedule(ctx, input.scheduleId);
139
+ if (settled(schedule))
140
+ return yield* Effect.fail(rejected("conflict", "A settled schedule cannot be paused"));
141
+ const next = paused(schedule);
142
+ yield* ctx.state.schedules.put(next);
143
+ yield* ctx.timers.cancel(schedule.id);
144
+ return next;
145
+ }),
146
+ resume: ({ input }) => Effect.gen(function* () {
147
+ const schedule = yield* requireSchedule(ctx, input.scheduleId);
148
+ if (settled(schedule))
149
+ return yield* Effect.fail(rejected("conflict", "A settled schedule cannot be resumed"));
150
+ const now = yield* Clock.currentTimeMillis;
151
+ // A human resume is the retry: it clears the failure and skips
152
+ // recurring occurrences missed while paused.
153
+ const back = resumed(schedule, now);
154
+ if (Result.isFailure(back))
155
+ return yield* Effect.fail(rejected("invalid", back.failure.message));
156
+ yield* ctx.state.schedules.put(back.success);
157
+ yield* ctx.timers.set(schedule.id, back.success.nextDueAt);
158
+ return back.success;
159
+ }),
160
+ cancel: ({ input }) => Effect.gen(function* () {
161
+ const schedule = yield* requireSchedule(ctx, input.scheduleId);
162
+ if (settled(schedule))
163
+ return schedule;
164
+ const next = cancelled(schedule);
165
+ yield* ctx.state.schedules.put(next);
166
+ yield* ctx.timers.cancel(schedule.id);
167
+ return next;
168
+ }),
169
+ }),
170
+ },
171
+ });
@@ -0,0 +1,10 @@
1
+ import { type ClientContributionV1 } from "@fungi.computer/shiitake/client-entry";
2
+ /** Product copy and controls belong to Goal; every host renders plain rows. */
3
+ declare const _default: Readonly<{
4
+ protocol: "shiitake.client/v1";
5
+ id: import("@fungi.computer/shiitake/extensions").ExtensionId;
6
+ correlatedPrompts?: "note";
7
+ attach?: (context: import("@fungi.computer/shiitake/client-entry").ClientExtensionContextV1) => import("effect/Effect").Effect<void, Error, import("effect/Scope").Scope>;
8
+ contribute?: (slot: import("@fungi.computer/shiitake").ExtensionSlotV0 | undefined) => ClientContributionV1;
9
+ }>;
10
+ export default _default;
@@ -0,0 +1,88 @@
1
+ import { defineClientEntry, } from "@fungi.computer/shiitake/client-entry";
2
+ import { goalContract } from "./contract.js";
3
+ const tone = (goal) => goal.state === "blocked"
4
+ ? "error"
5
+ : goal.state === "complete"
6
+ ? "settled"
7
+ : goal.state === "paused"
8
+ ? "muted"
9
+ : "running";
10
+ /** Product copy and controls belong to Goal; every host renders plain rows. */
11
+ export default defineClientEntry(goalContract, {
12
+ correlatedPrompts: "note",
13
+ contribute: (view) => {
14
+ if (view.status !== "running" || view.value === null)
15
+ return {
16
+ chips: [],
17
+ panel: [],
18
+ timeline: { cells: [], omitted: 0 },
19
+ waiting: null,
20
+ };
21
+ const { goal, history } = view.value;
22
+ const panel = [];
23
+ if (goal !== null) {
24
+ const row = {
25
+ id: "goal",
26
+ title: goal.objective,
27
+ detail: `Goal · ${goal.state}`,
28
+ tone: tone(goal),
29
+ };
30
+ panel.push(goal.state === "active" || goal.state === "paused"
31
+ ? {
32
+ ...row,
33
+ actions: [
34
+ {
35
+ id: "goal",
36
+ label: goal.state === "active" ? "Pause goal" : "Resume goal",
37
+ input: {
38
+ action: goal.state === "active" ? "pause" : "resume",
39
+ },
40
+ },
41
+ ],
42
+ }
43
+ : row);
44
+ if (goal.stopReason !== null)
45
+ panel.push({
46
+ id: "reason",
47
+ title: goal.stopReason.slice(0, 4096),
48
+ tone: "attention",
49
+ });
50
+ }
51
+ return {
52
+ chips: goal === null
53
+ ? []
54
+ : [
55
+ {
56
+ id: "goal",
57
+ order: 1,
58
+ row: {
59
+ id: "goal",
60
+ title: `Goal ${goal.state}${goal.stopReason?.startsWith("Stalled:") ? ": stalled" : ""}`,
61
+ tone: tone(goal),
62
+ },
63
+ },
64
+ ],
65
+ panel,
66
+ timeline: {
67
+ omitted: history.omitted,
68
+ cells: history.events.map(({ event, ...cell }) => ({
69
+ ...cell,
70
+ rows: [
71
+ { id: "state", title: `Goal ${event.state}`, tone: tone(event) },
72
+ { id: "objective", title: event.objective },
73
+ ...(event.stopReason === null
74
+ ? []
75
+ : [
76
+ {
77
+ id: "reason",
78
+ title: event.stopReason.slice(0, 4096),
79
+ tone: tone(event),
80
+ },
81
+ ]),
82
+ ],
83
+ })),
84
+ },
85
+ waiting: null,
86
+ };
87
+ },
88
+ });
@@ -0,0 +1,172 @@
1
+ import * as Schema from "effect/Schema";
2
+ /**
3
+ * Session goal: one durable objective per Session that Shiitake keeps
4
+ * working toward until the model reports it complete or blocked. Shared by
5
+ * the server extension and every client; its schemas are the one decoder.
6
+ */
7
+ /** A goal objective: stored exactly as sent, never blank. */
8
+ export declare const Objective: Schema.String;
9
+ export declare const GoalReason: Schema.String;
10
+ /** The committed goal every client sees; provisional terminals stay private. */
11
+ export declare const Goal: Schema.Struct<{
12
+ /** Owner-issued revision that guards an explicit replacement. */
13
+ readonly revision: Schema.String;
14
+ readonly objective: Schema.String;
15
+ readonly state: Schema.Literals<readonly ["active", "paused", "complete", "blocked"]>;
16
+ /** Why continuation stopped (for example failed work); a resume retries. */
17
+ readonly stopReason: Schema.NullOr<Schema.String>;
18
+ }>;
19
+ /** The committed goal every client sees. */
20
+ export type Goal = Schema.Schema.Type<typeof Goal>;
21
+ /**
22
+ * Arguments of the human `/goal` command. A bare call opens the controls; each
23
+ * reply is a catalog step, so the conversation stays on the server.
24
+ */
25
+ export declare const GoalCommandInput: Schema.Union<readonly [Schema.Struct<{
26
+ readonly action: Schema.Literal<"set">;
27
+ readonly objective: Schema.String;
28
+ readonly requestId: Schema.String;
29
+ /** The revision the human confirmed replacing, when they confirmed one. */
30
+ readonly expectedRevision: Schema.optional<Schema.NullOr<Schema.String>>;
31
+ /** The human's answer to "replace the current goal?". */
32
+ readonly confirmed: Schema.optional<Schema.Boolean>;
33
+ }>, Schema.Struct<{
34
+ readonly action: Schema.Literals<readonly ["pause", "resume", "replace", "clear"]>;
35
+ }>, Schema.Struct<{}>]>;
36
+ /** Goal's contract: Session-scope human controls and one projection slot. */
37
+ export declare const GoalProjection: Schema.Struct<{
38
+ readonly goal: Schema.NullOr<Schema.Struct<{
39
+ /** Owner-issued revision that guards an explicit replacement. */
40
+ readonly revision: Schema.String;
41
+ readonly objective: Schema.String;
42
+ readonly state: Schema.Literals<readonly ["active", "paused", "complete", "blocked"]>;
43
+ /** Why continuation stopped (for example failed work); a resume retries. */
44
+ readonly stopReason: Schema.NullOr<Schema.String>;
45
+ }>>;
46
+ readonly history: Schema.Struct<{
47
+ readonly nextSequence: Schema.Int;
48
+ readonly omitted: Schema.Int;
49
+ readonly events: Schema.$Array<Schema.Struct<{
50
+ readonly id: Schema.String;
51
+ readonly sequence: Schema.Int;
52
+ readonly at: Schema.Int;
53
+ readonly commandId: Schema.NullOr<Schema.String>;
54
+ readonly event: Schema.Codec<{
55
+ readonly revision: string;
56
+ readonly objective: string;
57
+ readonly state: "active" | "paused" | "complete" | "blocked";
58
+ readonly stopReason: string | null;
59
+ }, {
60
+ readonly revision: string;
61
+ readonly objective: string;
62
+ readonly state: "active" | "paused" | "complete" | "blocked";
63
+ readonly stopReason: string | null;
64
+ }, never, never>;
65
+ }>>;
66
+ }>;
67
+ }>;
68
+ export declare const goalContract: {
69
+ readonly id: string & import("effect/Brand").Brand<"ExtensionId">;
70
+ readonly commands: {
71
+ readonly goal: {
72
+ readonly scope: "session";
73
+ readonly input: Schema.Union<readonly [Schema.Struct<{
74
+ readonly action: Schema.Literal<"set">;
75
+ readonly objective: Schema.String;
76
+ readonly requestId: Schema.String;
77
+ /** The revision the human confirmed replacing, when they confirmed one. */
78
+ readonly expectedRevision: Schema.optional<Schema.NullOr<Schema.String>>;
79
+ /** The human's answer to "replace the current goal?". */
80
+ readonly confirmed: Schema.optional<Schema.Boolean>;
81
+ }>, Schema.Struct<{
82
+ readonly action: Schema.Literals<readonly ["pause", "resume", "replace", "clear"]>;
83
+ }>, Schema.Struct<{}>]>;
84
+ readonly output: Schema.declare<import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1, import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1>;
85
+ };
86
+ readonly pause: {
87
+ readonly scope: "session";
88
+ readonly input: Schema.Struct<{}>;
89
+ readonly output: Schema.Struct<{
90
+ /** Owner-issued revision that guards an explicit replacement. */
91
+ readonly revision: Schema.String;
92
+ readonly objective: Schema.String;
93
+ readonly state: Schema.Literals<readonly ["active", "paused", "complete", "blocked"]>;
94
+ /** Why continuation stopped (for example failed work); a resume retries. */
95
+ readonly stopReason: Schema.NullOr<Schema.String>;
96
+ }>;
97
+ };
98
+ readonly resume: {
99
+ readonly scope: "session";
100
+ readonly input: Schema.Struct<{}>;
101
+ readonly output: Schema.Struct<{
102
+ /** Owner-issued revision that guards an explicit replacement. */
103
+ readonly revision: Schema.String;
104
+ readonly objective: Schema.String;
105
+ readonly state: Schema.Literals<readonly ["active", "paused", "complete", "blocked"]>;
106
+ /** Why continuation stopped (for example failed work); a resume retries. */
107
+ readonly stopReason: Schema.NullOr<Schema.String>;
108
+ }>;
109
+ };
110
+ readonly clear: {
111
+ readonly scope: "session";
112
+ readonly input: Schema.Struct<{}>;
113
+ readonly output: Schema.Struct<{}>;
114
+ };
115
+ readonly set: {
116
+ readonly scope: "session";
117
+ readonly input: Schema.Struct<{
118
+ readonly objective: Schema.String;
119
+ /** The revision the user saw, or null when they saw no goal. */
120
+ readonly expectedRevision: Schema.NullOr<Schema.String>;
121
+ readonly requestId: Schema.String;
122
+ }>;
123
+ readonly output: Schema.Struct<{
124
+ readonly goal: Schema.Struct<{
125
+ /** Owner-issued revision that guards an explicit replacement. */
126
+ readonly revision: Schema.String;
127
+ readonly objective: Schema.String;
128
+ readonly state: Schema.Literals<readonly ["active", "paused", "complete", "blocked"]>;
129
+ /** Why continuation stopped (for example failed work); a resume retries. */
130
+ readonly stopReason: Schema.NullOr<Schema.String>;
131
+ }>;
132
+ readonly accepted: Schema.Struct<{
133
+ readonly status: Schema.Literal<"accepted">;
134
+ readonly commandId: Schema.String;
135
+ }>;
136
+ }>;
137
+ };
138
+ };
139
+ readonly projection: {
140
+ readonly schema: Schema.Struct<{
141
+ readonly goal: Schema.NullOr<Schema.Struct<{
142
+ /** Owner-issued revision that guards an explicit replacement. */
143
+ readonly revision: Schema.String;
144
+ readonly objective: Schema.String;
145
+ readonly state: Schema.Literals<readonly ["active", "paused", "complete", "blocked"]>;
146
+ /** Why continuation stopped (for example failed work); a resume retries. */
147
+ readonly stopReason: Schema.NullOr<Schema.String>;
148
+ }>>;
149
+ readonly history: Schema.Struct<{
150
+ readonly nextSequence: Schema.Int;
151
+ readonly omitted: Schema.Int;
152
+ readonly events: Schema.$Array<Schema.Struct<{
153
+ readonly id: Schema.String;
154
+ readonly sequence: Schema.Int;
155
+ readonly at: Schema.Int;
156
+ readonly commandId: Schema.NullOr<Schema.String>;
157
+ readonly event: Schema.Codec<{
158
+ readonly revision: string;
159
+ readonly objective: string;
160
+ readonly state: "active" | "paused" | "complete" | "blocked";
161
+ readonly stopReason: string | null;
162
+ }, {
163
+ readonly revision: string;
164
+ readonly objective: string;
165
+ readonly state: "active" | "paused" | "complete" | "blocked";
166
+ readonly stopReason: string | null;
167
+ }, never, never>;
168
+ }>>;
169
+ }>;
170
+ }>;
171
+ };
172
+ };
@@ -0,0 +1,76 @@
1
+ import { historySchema } from "../internal/history.js";
2
+ import { ExtensionId } from "@fungi.computer/shiitake/extensions";
3
+ import * as Schema from "effect/Schema";
4
+ import { CatalogStep } from "../internal/catalog.js";
5
+ /**
6
+ * Session goal: one durable objective per Session that Shiitake keeps
7
+ * working toward until the model reports it complete or blocked. Shared by
8
+ * the server extension and every client; its schemas are the one decoder.
9
+ */
10
+ /** A goal objective: stored exactly as sent, never blank. */
11
+ export const Objective = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(4_000)), Schema.check(Schema.isPattern(/\S/u, { message: "Goal objective is blank" })));
12
+ export const GoalReason = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(1_000)), Schema.check(Schema.isPattern(/\S/u)));
13
+ const RequestId = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
14
+ /** The committed goal every client sees; provisional terminals stay private. */
15
+ export const Goal = Schema.Struct({
16
+ /** Owner-issued revision that guards an explicit replacement. */
17
+ revision: Schema.String,
18
+ objective: Objective,
19
+ state: Schema.Literals(["active", "paused", "complete", "blocked"]),
20
+ /** Why continuation stopped (for example failed work); a resume retries. */
21
+ stopReason: Schema.NullOr(Schema.String),
22
+ });
23
+ /**
24
+ * Arguments of the human `/goal` command. A bare call opens the controls; each
25
+ * reply is a catalog step, so the conversation stays on the server.
26
+ */
27
+ export const GoalCommandInput = Schema.Union([
28
+ Schema.Struct({
29
+ action: Schema.Literal("set"),
30
+ objective: Objective,
31
+ requestId: RequestId,
32
+ /** The revision the human confirmed replacing, when they confirmed one. */
33
+ expectedRevision: Schema.optional(Schema.NullOr(Schema.String)),
34
+ /** The human's answer to "replace the current goal?". */
35
+ confirmed: Schema.optional(Schema.Boolean),
36
+ }),
37
+ Schema.Struct({
38
+ action: Schema.Literals(["pause", "resume", "replace", "clear"]),
39
+ }),
40
+ Schema.Struct({}),
41
+ ]);
42
+ /** Goal's contract: Session-scope human controls and one projection slot. */
43
+ export const GoalProjection = Schema.Struct({
44
+ goal: Schema.NullOr(Goal),
45
+ history: historySchema(Goal),
46
+ });
47
+ export const goalContract = {
48
+ id: ExtensionId.make("botanical-session-goal"),
49
+ commands: {
50
+ goal: { scope: "session", input: GoalCommandInput, output: CatalogStep },
51
+ pause: { scope: "session", input: Schema.Struct({}), output: Goal },
52
+ resume: { scope: "session", input: Schema.Struct({}), output: Goal },
53
+ clear: {
54
+ scope: "session",
55
+ input: Schema.Struct({}),
56
+ output: Schema.Struct({}),
57
+ },
58
+ set: {
59
+ scope: "session",
60
+ input: Schema.Struct({
61
+ objective: Objective,
62
+ /** The revision the user saw, or null when they saw no goal. */
63
+ expectedRevision: Schema.NullOr(Schema.String),
64
+ requestId: RequestId,
65
+ }),
66
+ output: Schema.Struct({
67
+ goal: Goal,
68
+ accepted: Schema.Struct({
69
+ status: Schema.Literal("accepted"),
70
+ commandId: Schema.String,
71
+ }),
72
+ }),
73
+ },
74
+ },
75
+ projection: { schema: GoalProjection },
76
+ };
@@ -0,0 +1,5 @@
1
+ import { type RosteredExtensionV0 } from "@fungi.computer/shiitake/extensions";
2
+ /** The commands this extension advertises to every client. */
3
+ export declare const goalCatalog: import("@fungi.computer/shiitake/extensions").Json;
4
+ /** Botanical's Session goal, driven only by settled runs. */
5
+ export declare const goalExtension: RosteredExtensionV0;