@fungi.computer/shiitake-extensions 0.0.2 → 0.1.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 (66) hide show
  1. package/README.md +43 -13
  2. package/dist/almanac/contract.d.ts +94 -0
  3. package/dist/almanac/contract.js +71 -5
  4. package/dist/goal/client.d.ts +5 -2
  5. package/dist/goal/client.js +66 -72
  6. package/dist/goal/contract.d.ts +181 -64
  7. package/dist/goal/contract.js +102 -12
  8. package/dist/goal/host.js +206 -124
  9. package/dist/home/contract.d.ts +18 -0
  10. package/dist/home/contract.js +24 -3
  11. package/dist/home/host.d.ts +3 -0
  12. package/dist/home/host.js +1 -2
  13. package/dist/home/routing.js +13 -24
  14. package/dist/internal/history.d.ts +16 -0
  15. package/dist/internal/history.js +8 -0
  16. package/dist/loop/client.d.ts +1 -1
  17. package/dist/loop/client.js +28 -34
  18. package/dist/loop/contract.d.ts +129 -84
  19. package/dist/loop/contract.js +79 -9
  20. package/dist/loop/host.d.ts +1 -0
  21. package/dist/loop/host.js +89 -48
  22. package/dist/plan/client.d.ts +1 -1
  23. package/dist/plan/contract.d.ts +33 -0
  24. package/dist/plan/contract.js +28 -3
  25. package/dist/plan/host.js +12 -7
  26. package/dist/question/client.d.ts +1 -1
  27. package/dist/question/contract.d.ts +158 -0
  28. package/dist/question/contract.js +92 -7
  29. package/dist/question/host.js +34 -20
  30. package/dist/roster/conversation-agents.d.ts +31 -0
  31. package/dist/roster/conversation-agents.js +114 -0
  32. package/dist/roster.js +1 -0
  33. package/dist/skills/host.d.ts +1 -0
  34. package/dist/skills/host.js +22 -17
  35. package/dist/skills/source.d.ts +34 -0
  36. package/dist/subagents/client.d.ts +1 -1
  37. package/dist/subagents/client.js +43 -12
  38. package/dist/subagents/contract.d.ts +46 -8
  39. package/dist/subagents/contract.js +38 -8
  40. package/dist/subagents/host.d.ts +20 -8
  41. package/dist/subagents/host.js +58 -12
  42. package/docs/api/README.md +35 -0
  43. package/docs/api/almanac/contract.md +90 -0
  44. package/docs/api/almanac/host.md +19 -0
  45. package/docs/api/goal/client.md +26 -0
  46. package/docs/api/goal/contract.md +247 -0
  47. package/docs/api/goal/host.md +29 -0
  48. package/docs/api/home/contract.md +58 -0
  49. package/docs/api/home/host.md +55 -0
  50. package/docs/api/loop/client.md +25 -0
  51. package/docs/api/loop/contract.md +161 -0
  52. package/docs/api/loop/host.md +30 -0
  53. package/docs/api/plan/client.md +26 -0
  54. package/docs/api/plan/contract.md +163 -0
  55. package/docs/api/plan/host.md +19 -0
  56. package/docs/api/question/client.md +25 -0
  57. package/docs/api/question/contract.md +377 -0
  58. package/docs/api/question/host.md +19 -0
  59. package/docs/api/roster/conversation-agents.md +80 -0
  60. package/docs/api/roster.md +40 -0
  61. package/docs/api/skills/host.md +39 -0
  62. package/docs/api/skills/source.md +104 -0
  63. package/docs/api/subagents/client.md +26 -0
  64. package/docs/api/subagents/contract.md +131 -0
  65. package/docs/api/subagents/host.md +115 -0
  66. package/package.json +14 -8
package/dist/loop/host.js CHANGED
@@ -1,5 +1,4 @@
1
- import { appendHistory, emptyHistory, historySchema, } from "../internal/history.js";
2
- import { Mycelium } from "@fungi.computer/mycelium";
1
+ import { CapabilityFailed, Mycelium, } from "@fungi.computer/mycelium";
3
2
  import { defineCollection, defineExtension, ExtensionCommandError, ExtensionStop, } from "@fungi.computer/shiitake/extensions";
4
3
  import * as Cause from "effect/Cause";
5
4
  import * as Clock from "effect/Clock";
@@ -12,14 +11,19 @@ import { z } from "zod";
12
11
  import { ask, catalog, done, freshRequestId } from "../internal/catalog.js";
13
12
  import { nextOccurrence } from "../internal/cron.js";
14
13
  import { Loop, LoopCommandInput, loopContract } from "./contract.js";
14
+ /** The rules the model follows; the describe page and a live loop's prompt share them. */
15
+ const rules = "You may start loops yourself; one live loop per Session. Failed loop ticks retry with backoff; loop.get shows consecutiveFailures, lastError, and nextDueAt. schedule_next is allowed only in the active self-paced loop's own run, clamped to 60–3600 seconds. Omitting it ends that loop. stop works in any Session run. Human pause/resume/stop and Esc remain user controls.";
15
16
  /** Hashed prose and policy: every constant loop reads comes from here. */
16
17
  const config = {
17
18
  minDelaySeconds: 60,
18
19
  maxDelaySeconds: 3_600,
19
20
  fixedLifetimeMs: 7 * 24 * 60 * 60 * 1_000,
20
21
  promptLimit: 1_024,
21
- guide: "Session loop tools in execute: loop.get({}), loop.start({prompt,cadence}), loop.schedule_next({delaySeconds,reason}), loop.stop({reason}). You may start loops yourself; one live loop per Session. Find them with forage.search({query:'loop'}) or 'schedule' and forage.describe. schedule_next is allowed only in the active self-paced loop's own run, clamped to 60–3600 seconds. Omitting it ends that loop. stop works in any Session run. Human pause/resume/stop and Esc remain user controls.",
22
- moduleDescription: "Session loop control and schedule tools. Start, get or stop a loop from any Session run; schedule_next requires the self-paced loop's current run.",
22
+ idle: "Recurring or scheduled work? forage.describe({path:'loop'}) first.",
23
+ guide: "Session loop tools: loop.get, loop.start, loop.schedule_next, loop.stop. " +
24
+ rules,
25
+ moduleDescription: "Session loop tools: get({}), start({prompt,cadence}), schedule_next({delaySeconds,reason}), stop({reason}). " +
26
+ rules,
23
27
  scheduleDescription: "Schedule the original loop prompt after this run settles successfully. delaySeconds is clamped between 60 and 3600. The reason is stored as why now and does not change the loop prompt. Do not call this when the loop should end.",
24
28
  };
25
29
  /** One Session's durable loop row. */
@@ -34,9 +38,9 @@ const LoopRow = Schema.Struct({
34
38
  delaySeconds: Schema.Number,
35
39
  reason: Schema.String,
36
40
  })),
41
+ consecutiveFailures: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.withDecodingDefaultKey(Effect.succeed(0))),
37
42
  });
38
43
  const collections = {
39
- history: defineCollection(Schema.Struct({ sessionId: Schema.String, history: historySchema(Loop) }), { key: (row) => row.sessionId }),
40
44
  loops: defineCollection(LoopRow, {
41
45
  key: (loop) => loop.sessionId,
42
46
  index: (loop) => loop.state,
@@ -58,23 +62,11 @@ function projected(row) {
58
62
  reason: row.reason,
59
63
  nextDueAt: row.nextDueAt,
60
64
  lastError: row.lastError,
65
+ consecutiveFailures: row.consecutiveFailures,
61
66
  };
62
67
  }
63
68
  function commitLoop(ctx, row) {
64
69
  return Effect.gen(function* () {
65
- const previous = Option.getOrUndefined(yield* ctx.state.loops.get(row.sessionId));
66
- if (previous?.state !== row.state &&
67
- (row.state === "active" ||
68
- row.state === "finished" ||
69
- row.state === "cancelled" ||
70
- row.state === "failed")) {
71
- const history = Option.getOrUndefined(yield* ctx.state.history.get(row.sessionId))
72
- ?.history ?? emptyHistory();
73
- yield* ctx.state.history.put({
74
- sessionId: row.sessionId,
75
- history: appendHistory(history, projected(row), row.commandId || null, yield* Clock.currentTimeMillis),
76
- });
77
- }
78
70
  yield* ctx.state.loops.put(row);
79
71
  });
80
72
  }
@@ -137,6 +129,7 @@ function start(ctx, sessionId, input, cron, origin = "human") {
137
129
  lastError: null,
138
130
  startRequestId: input.requestId,
139
131
  requested: null,
132
+ consecutiveFailures: 0,
140
133
  };
141
134
  const stored = cron === null
142
135
  ? row
@@ -160,6 +153,8 @@ function requireLoop(ctx, sessionId) {
160
153
  }
161
154
  /** Where a resumed loop picks up: its cadence, or its recorded next instant. */
162
155
  function resumeAt(loop, now) {
156
+ if (loop.consecutiveFailures > 0)
157
+ return Effect.succeed(loop.nextDueAt === null ? null : Math.max(now, loop.nextDueAt));
163
158
  return loop.cron === null
164
159
  ? Effect.succeed(loop.nextDueAt)
165
160
  : nextOccurrence(loop.cron, now);
@@ -175,6 +170,27 @@ function settle(ctx, run) {
175
170
  return;
176
171
  if (run.workId !== loop.commandId)
177
172
  return;
173
+ if (run.outcome === "failed") {
174
+ const consecutiveFailures = Math.min(Number.MAX_SAFE_INTEGER, loop.consecutiveFailures + 1);
175
+ const backoffExponent = Math.min(consecutiveFailures - 1, 5);
176
+ const delayMs = Math.min(15 * 60_000, 30_000 * 2 ** backoffExponent);
177
+ const now = yield* Clock.currentTimeMillis;
178
+ const dueAt = now + delayMs;
179
+ const retry = {
180
+ ...loop,
181
+ state: loop.state,
182
+ requested: null,
183
+ reason: null,
184
+ nextDueAt: dueAt,
185
+ lastError: "Loop work ended with outcome failed.",
186
+ consecutiveFailures,
187
+ };
188
+ if (loop.state === "paused")
189
+ yield* commitLoop(ctx, retry);
190
+ else
191
+ yield* arm(ctx, retry, dueAt);
192
+ return yield* ctx.observe(loop.sessionId);
193
+ }
178
194
  if (run.outcome !== "completed" || run.terminal !== "completed") {
179
195
  yield* ctx.timers.cancel(loop.sessionId);
180
196
  yield* commitLoop(ctx, {
@@ -186,12 +202,22 @@ function settle(ctx, run) {
186
202
  lastError: run.outcome === "completed"
187
203
  ? "Completed loop work has no exact terminal."
188
204
  : `Loop work ended with outcome ${run.outcome}.`,
205
+ consecutiveFailures: loop.consecutiveFailures,
189
206
  });
190
207
  return yield* ctx.observe(loop.sessionId);
191
208
  }
192
209
  // A fixed interval loop's cadence is armed independently of its runs.
193
- if (loop.mode === "fixed_interval")
210
+ if (loop.mode === "fixed_interval") {
211
+ if (loop.consecutiveFailures !== 0 || loop.lastError !== null) {
212
+ yield* commitLoop(ctx, {
213
+ ...loop,
214
+ consecutiveFailures: 0,
215
+ lastError: null,
216
+ });
217
+ yield* ctx.observe(loop.sessionId);
218
+ }
194
219
  return;
220
+ }
195
221
  // Omitting schedule_next during the run ends a self-paced loop.
196
222
  if (loop.requested?.workId !== run.workId) {
197
223
  yield* commitLoop(ctx, {
@@ -199,6 +225,8 @@ function settle(ctx, run) {
199
225
  state: "finished",
200
226
  reason: "no next wake requested",
201
227
  nextDueAt: null,
228
+ lastError: null,
229
+ consecutiveFailures: 0,
202
230
  });
203
231
  return yield* ctx.observe(loop.sessionId);
204
232
  }
@@ -208,6 +236,8 @@ function settle(ctx, run) {
208
236
  reason: loop.requested.reason,
209
237
  requested: null,
210
238
  nextDueAt: now + loop.requested.delaySeconds * 1_000,
239
+ lastError: null,
240
+ consecutiveFailures: 0,
211
241
  };
212
242
  // A paused loop keeps the instant; resume arms it.
213
243
  if (loop.state === "paused")
@@ -283,17 +313,24 @@ function fire(ctx, sessionId, dueAt, now) {
283
313
  yield* ctx.observe(sessionId);
284
314
  });
285
315
  }
286
- /** Run one tool transaction and surface its failure to the model as an Error. */
287
- function toolCall(ctx, body) {
316
+ /** Run one tool transaction and preserve its owner-admitted diagnostic. */
317
+ function toolCall(ctx, operation, body) {
288
318
  return Effect.runPromiseExit(ctx.transact(body)).then((exit) => {
289
319
  if (Exit.isSuccess(exit))
290
320
  return exit.value;
291
321
  const failure = Cause.squash(exit.cause);
292
- return Promise.reject(new Error(failure instanceof ExtensionStop
293
- ? failure.reason
294
- : failure instanceof Error
295
- ? failure.message
296
- : "Loop tool failed"));
322
+ return Promise.reject(new CapabilityFailed({
323
+ namespace: "loop",
324
+ operation,
325
+ cause: {
326
+ kind: "OperationFailed",
327
+ message: failure instanceof ExtensionStop
328
+ ? failure.reason
329
+ : failure instanceof Error
330
+ ? failure.message
331
+ : "Loop tool failed",
332
+ },
333
+ }));
297
334
  });
298
335
  }
299
336
  // Mycelium's maintained operation interface takes Zod for model-tool I/O.
@@ -315,6 +352,7 @@ const toolLoop = z.strictObject({
315
352
  reason: z.string().nullable(),
316
353
  nextDueAt: z.number().nullable(),
317
354
  lastError: z.string().nullable(),
355
+ consecutiveFailures: z.number().int().nonnegative(),
318
356
  });
319
357
  /** The self-paced `schedule_next` tool for exactly the loop's current run. */
320
358
  function loopModule(ctx, sessionId, workId) {
@@ -328,7 +366,7 @@ function loopModule(ctx, sessionId, workId) {
328
366
  description: "Get the current Session loop and its next scheduled wake.",
329
367
  input: z.strictObject({}),
330
368
  output: toolLoop.nullable(),
331
- execute: () => toolCall(ctx, ctx.state.loops
369
+ execute: () => toolCall(ctx, "get", ctx.state.loops
332
370
  .get(sessionId)
333
371
  .pipe(Effect.map((found) => Option.isSome(found) ? projected(found.value) : null))),
334
372
  }),
@@ -353,7 +391,7 @@ function loopModule(ctx, sessionId, workId) {
353
391
  ]),
354
392
  }),
355
393
  output: toolLoop,
356
- execute: ({ prompt, cadence }) => toolCall(ctx, Effect.gen(function* () {
394
+ execute: ({ prompt, cadence }) => toolCall(ctx, "start", Effect.gen(function* () {
357
395
  const requestId = yield* freshRequestId;
358
396
  const cron = cadence.kind === "self_paced"
359
397
  ? null
@@ -372,7 +410,7 @@ function loopModule(ctx, sessionId, workId) {
372
410
  description: "Stop this Session's loop from any run, with a visible reason. Cancel its next scheduled wake.",
373
411
  input: z.strictObject({ reason: z.string().trim().min(1).max(1_000) }),
374
412
  output: toolLoop,
375
- execute: ({ reason }) => toolCall(ctx, stopLoop(ctx, sessionId, reason)),
413
+ execute: ({ reason }) => toolCall(ctx, "stop", stopLoop(ctx, sessionId, reason)),
376
414
  }),
377
415
  schedule_next: Mycelium.operation({
378
416
  replay: "unsafe",
@@ -383,13 +421,16 @@ function loopModule(ctx, sessionId, workId) {
383
421
  dueAt: z.number(),
384
422
  reason: z.string().min(1).max(4_000),
385
423
  }),
386
- execute: ({ delaySeconds, reason }) => toolCall(ctx, Effect.gen(function* () {
424
+ execute: ({ delaySeconds, reason }) => toolCall(ctx, "schedule_next", Effect.gen(function* () {
387
425
  const found = yield* ctx.state.loops.get(sessionId);
388
- if (Option.isNone(found) ||
389
- found.value.state !== "active" ||
426
+ if (Option.isNone(found))
427
+ return yield* Effect.fail(conflict("schedule_next only works inside this loop's own tick run"));
428
+ if (found.value.state === "failed")
429
+ return yield* Effect.fail(conflict("loop is failed; start a new one with loop.start"));
430
+ if (found.value.state !== "active" ||
390
431
  found.value.mode !== "self_paced" ||
391
432
  found.value.commandId !== workId)
392
- return yield* Effect.fail(conflict("Loop work guard is stale"));
433
+ return yield* Effect.fail(conflict("schedule_next only works inside this loop's own tick run"));
393
434
  const clamped = Math.max(ctx.config.minDelaySeconds, Math.min(ctx.config.maxDelaySeconds, delaySeconds));
394
435
  yield* commitLoop(ctx, {
395
436
  ...found.value,
@@ -582,23 +623,27 @@ export const loopCatalog = catalog([
582
623
  },
583
624
  },
584
625
  ]);
626
+ /** Durable Session loop implementation with prompts, controls and scheduled occurrences. */
585
627
  export const loopExtension = defineExtension({
586
628
  id: loopContract.id,
587
- version: "3.0.0",
629
+ version: "4.0.0",
588
630
  })
589
631
  .config(config)
590
- .state({ version: 3, migrations: {}, collections })
632
+ .state({
633
+ version: 3,
634
+ collections,
635
+ })
591
636
  .define({
592
637
  prompt: (ctx, { sessionId }) => ctx.state.loops.get(sessionId).pipe(Effect.map((found) => [
593
638
  {
594
639
  name: "loop",
595
- text: (ctx.config.guide +
596
- "\n" +
597
- (Option.isSome(found)
598
- ? loopLine(projected(found.value)) +
599
- "; nextDueAt=" +
600
- String(found.value.nextDueAt)
601
- : "No current loop.")).slice(0, ctx.config.promptLimit),
640
+ text: Option.isSome(found)
641
+ ? (ctx.config.guide +
642
+ "\n" +
643
+ loopLine(projected(found.value)) +
644
+ "; nextDueAt=" +
645
+ String(found.value.nextDueAt)).slice(0, ctx.config.promptLimit)
646
+ : ctx.config.idle,
602
647
  },
603
648
  ])),
604
649
  catalog: loopCatalog,
@@ -606,13 +651,9 @@ export const loopExtension = defineExtension({
606
651
  schema: loopContract.projection.schema,
607
652
  read: (ctx, sessionId) => {
608
653
  const loop = Option.getOrUndefined(ctx.state.loops.peek(sessionId));
609
- const history = Option.getOrUndefined(ctx.state.history.peek(sessionId))?.history;
610
- return loop === undefined && history === undefined
654
+ return loop === undefined
611
655
  ? Option.none()
612
- : Option.some({
613
- loop: loop === undefined ? null : projected(loop),
614
- history: history ?? emptyHistory(),
615
- });
656
+ : Option.some({ loop: projected(loop) });
616
657
  },
617
658
  },
618
659
  hooks: {
@@ -6,7 +6,7 @@ import { type ClientContributionV1 } from "@fungi.computer/shiitake/client-entry
6
6
  declare const _default: Readonly<{
7
7
  protocol: "shiitake.client/v1";
8
8
  id: import("@fungi.computer/shiitake/extensions").ExtensionId;
9
- correlatedPrompts?: "note";
9
+ promptPresentation?: "footer";
10
10
  attach?: (context: import("@fungi.computer/shiitake/client-entry").ClientExtensionContextV1) => import("effect/Effect").Effect<void, Error, import("effect/Scope").Scope>;
11
11
  contribute?: (slot: import("@fungi.computer/shiitake").ExtensionSlotV0 | undefined) => ClientContributionV1;
12
12
  }>;
@@ -4,31 +4,47 @@ import * as Schema from "effect/Schema";
4
4
  * steps means no plan: replacing it with an empty checklist clears it.
5
5
  */
6
6
  export declare const Plan: Schema.Struct<{
7
+ /** Optional explanation for this checklist replacement. */
7
8
  readonly explanation: Schema.optionalKey<Schema.String>;
9
+ /** Ordered checklist; an empty array clears the plan. */
8
10
  readonly steps: Schema.$Array<Schema.Struct<{
11
+ /** Work described by this checklist step. */
9
12
  readonly step: Schema.String;
13
+ /** Current progress state of the step. */
10
14
  readonly status: Schema.Literals<readonly ["pending", "in_progress", "completed"]>;
11
15
  }>>;
12
16
  }>;
17
+ /** Checklist replacement that permits at most one in-progress step. */
13
18
  export declare const PlanInput: Schema.Struct<{
19
+ /** Optional explanation for this checklist replacement. */
14
20
  readonly explanation: Schema.optionalKey<Schema.String>;
21
+ /** Ordered checklist; an empty array clears the plan. */
15
22
  readonly steps: Schema.$Array<Schema.Struct<{
23
+ /** Work described by this checklist step. */
16
24
  readonly step: Schema.String;
25
+ /** Current progress state of the step. */
17
26
  readonly status: Schema.Literals<readonly ["pending", "in_progress", "completed"]>;
18
27
  }>>;
19
28
  }>;
29
+ /** One Session's ordered checklist and optional explanation. */
20
30
  export type Plan = Schema.Schema.Type<typeof Plan>;
21
31
  /** Open work remains: some step is not completed. Cleared and finished plans are closed. */
22
32
  export declare function planIsOpen(plan: Plan): boolean;
23
33
  /** The current checklist and every committed replacement, clears included. */
24
34
  export declare const PlanProjection: Schema.Struct<{
35
+ /** Latest checklist replacement. */
25
36
  readonly plan: Schema.Struct<{
37
+ /** Optional explanation for this checklist replacement. */
26
38
  readonly explanation: Schema.optionalKey<Schema.String>;
39
+ /** Ordered checklist; an empty array clears the plan. */
27
40
  readonly steps: Schema.$Array<Schema.Struct<{
41
+ /** Work described by this checklist step. */
28
42
  readonly step: Schema.String;
43
+ /** Current progress state of the step. */
29
44
  readonly status: Schema.Literals<readonly ["pending", "in_progress", "completed"]>;
30
45
  }>>;
31
46
  }>;
47
+ /** Committed replacements, including explicit clears. */
32
48
  readonly history: Schema.Struct<{
33
49
  readonly nextSequence: Schema.Int;
34
50
  readonly omitted: Schema.Int;
@@ -55,28 +71,45 @@ export declare const PlanProjection: Schema.Struct<{
55
71
  }>;
56
72
  /** Arguments of the human `/plan` command: a bare call views, `clear` empties it. */
57
73
  export declare const PlanCommandInput: Schema.Union<readonly [Schema.Struct<{
74
+ /** Clear the checklist instead of viewing it. */
58
75
  readonly action: Schema.Literal<"clear">;
59
76
  }>, Schema.Struct<{}>]>;
77
+ /** Session-owned human plan control and checklist projection. */
60
78
  export declare const planContract: {
79
+ /** Stable extension identity. */
61
80
  id: string & import("effect/Brand").Brand<"ExtensionId">;
81
+ /** Human checklist controls. */
62
82
  commands: {
83
+ /** View or clear the current checklist. */
63
84
  plan: {
85
+ /** The Session owns the checklist. */
64
86
  scope: "session";
87
+ /** Decode the selected human control. */
65
88
  input: Schema.Union<readonly [Schema.Struct<{
89
+ /** Clear the checklist instead of viewing it. */
66
90
  readonly action: Schema.Literal<"clear">;
67
91
  }>, Schema.Struct<{}>]>;
92
+ /** Return the server-owned conversation step. */
68
93
  output: Schema.declare<import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1, import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1>;
69
94
  };
70
95
  };
96
+ /** Current checklist and its replacement history. */
71
97
  projection: {
98
+ /** Decoder for visible plan state. */
72
99
  schema: Schema.Struct<{
100
+ /** Latest checklist replacement. */
73
101
  readonly plan: Schema.Struct<{
102
+ /** Optional explanation for this checklist replacement. */
74
103
  readonly explanation: Schema.optionalKey<Schema.String>;
104
+ /** Ordered checklist; an empty array clears the plan. */
75
105
  readonly steps: Schema.$Array<Schema.Struct<{
106
+ /** Work described by this checklist step. */
76
107
  readonly step: Schema.String;
108
+ /** Current progress state of the step. */
77
109
  readonly status: Schema.Literals<readonly ["pending", "in_progress", "completed"]>;
78
110
  }>>;
79
111
  }>;
112
+ /** Committed replacements, including explicit clears. */
80
113
  readonly history: Schema.Struct<{
81
114
  readonly nextSequence: Schema.Int;
82
115
  readonly omitted: Schema.Int;
@@ -7,12 +7,17 @@ import { CatalogStep } from "../internal/catalog.js";
7
7
  * steps means no plan: replacing it with an empty checklist clears it.
8
8
  */
9
9
  export const Plan = Schema.Struct({
10
+ /** Optional explanation for this checklist replacement. */
10
11
  explanation: Schema.optionalKey(Schema.String.pipe(Schema.check(Schema.isMaxLength(2_000)))),
12
+ /** Ordered checklist; an empty array clears the plan. */
11
13
  steps: Schema.Array(Schema.Struct({
14
+ /** Work described by this checklist step. */
12
15
  step: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(500))),
16
+ /** Current progress state of the step. */
13
17
  status: Schema.Literals(["pending", "in_progress", "completed"]),
14
18
  })).pipe(Schema.check(Schema.isMaxLength(50))),
15
19
  });
20
+ /** Checklist replacement that permits at most one in-progress step. */
16
21
  export const PlanInput = Plan.pipe(Schema.check(Schema.makeFilter((plan) => plan.steps.filter((step) => step.status === "in_progress").length <=
17
22
  1 || "At most one plan step may be in progress")));
18
23
  /** Open work remains: some step is not completed. Cleared and finished plans are closed. */
@@ -21,18 +26,38 @@ export function planIsOpen(plan) {
21
26
  }
22
27
  /** The current checklist and every committed replacement, clears included. */
23
28
  export const PlanProjection = Schema.Struct({
29
+ /** Latest checklist replacement. */
24
30
  plan: Plan,
31
+ /** Committed replacements, including explicit clears. */
25
32
  history: historySchema(Plan),
26
33
  });
27
34
  /** Arguments of the human `/plan` command: a bare call views, `clear` empties it. */
28
35
  export const PlanCommandInput = Schema.Union([
29
- Schema.Struct({ action: Schema.Literal("clear") }),
36
+ Schema.Struct({
37
+ /** Clear the checklist instead of viewing it. */
38
+ action: Schema.Literal("clear"),
39
+ }),
30
40
  Schema.Struct({}),
31
41
  ]);
42
+ /** Session-owned human plan control and checklist projection. */
32
43
  export const planContract = {
44
+ /** Stable extension identity. */
33
45
  id: ExtensionId.make("botanical-session-plan"),
46
+ /** Human checklist controls. */
34
47
  commands: {
35
- plan: { scope: "session", input: PlanCommandInput, output: CatalogStep },
48
+ /** View or clear the current checklist. */
49
+ plan: {
50
+ /** The Session owns the checklist. */
51
+ scope: "session",
52
+ /** Decode the selected human control. */
53
+ input: PlanCommandInput,
54
+ /** Return the server-owned conversation step. */
55
+ output: CatalogStep,
56
+ },
57
+ },
58
+ /** Current checklist and its replacement history. */
59
+ projection: {
60
+ /** Decoder for visible plan state. */
61
+ schema: PlanProjection,
36
62
  },
37
- projection: { schema: PlanProjection },
38
63
  };
package/dist/plan/host.js CHANGED
@@ -8,9 +8,14 @@ import * as Schema from "effect/Schema";
8
8
  import { z } from "zod";
9
9
  import { catalog, done } from "../internal/catalog.js";
10
10
  import { Plan, PlanInput, planContract, planIsOpen } from "./contract.js";
11
+ /** The rules the model follows; the describe page and an open plan's prompt share them. */
12
+ const rules = "Use it for multi-step work, with at most one in-progress step; replace the whole plan on each update, and pass steps:[] to clear a plan that no longer applies. A completed checklist is not proof of goal completion.";
11
13
  const config = Object.freeze({
12
14
  promptLimit: 2_000,
13
- guide: "Session checklist tool in execute: plan.update_plan({explanation?,steps:[{step,status:'pending'|'in_progress'|'completed'}]}). Find it with forage.search({query:'plan'}) and forage.describe. Use it for multi-step work, with at most one in-progress step; replace the whole plan on each update, and pass steps:[] to clear a plan that no longer applies. A completed checklist is not proof of goal completion.",
15
+ idle: "Multi-step work? forage.describe({path:'plan'}) first.",
16
+ moduleDescription: "Session checklist tool: update_plan({explanation?,steps:[{step,status:'pending'|'in_progress'|'completed'}]}). " +
17
+ rules,
18
+ guide: "Session checklist tool: plan.update_plan. " + rules,
14
19
  });
15
20
  const collections = {
16
21
  history: defineCollection(Schema.Struct({ sessionId: Schema.String, history: historySchema(Plan) }), { key: (row) => row.sessionId }),
@@ -93,17 +98,17 @@ export const planExtension = defineExtension({
93
98
  prompt: (ctx, { sessionId }) => ctx.state.plans.get(sessionId).pipe(Effect.map((found) => [
94
99
  {
95
100
  name: "plan",
96
- text: (ctx.config.guide +
97
- "\n" +
98
- (Option.isSome(found) && found.value.plan.steps.length > 0
99
- ? JSON.stringify(found.value.plan)
100
- : "No current plan.")).slice(0, ctx.config.promptLimit),
101
+ text: Option.isSome(found) && found.value.plan.steps.length > 0
102
+ ? (ctx.config.guide +
103
+ "\n" +
104
+ JSON.stringify(found.value.plan)).slice(0, ctx.config.promptLimit)
105
+ : ctx.config.idle,
101
106
  },
102
107
  ])),
103
108
  tools: (ctx, { sessionId, workId }) => Effect.succeed(Option.some(Mycelium.module({
104
109
  id: "botanical.session-plan",
105
110
  name: "plan",
106
- description: "Session checklist planning tools.",
111
+ description: ctx.config.moduleDescription,
107
112
  operations: {
108
113
  update_plan: Mycelium.operation({
109
114
  replay: "safe",
@@ -3,7 +3,7 @@ import { type ClientContributionV1 } from "@fungi.computer/shiitake/client-entry
3
3
  declare const _default: Readonly<{
4
4
  protocol: "shiitake.client/v1";
5
5
  id: import("@fungi.computer/shiitake/extensions").ExtensionId;
6
- correlatedPrompts?: "note";
6
+ promptPresentation?: "footer";
7
7
  attach?: (context: import("@fungi.computer/shiitake/client-entry").ClientExtensionContextV1) => import("effect/Effect").Effect<void, Error, import("effect/Scope").Scope>;
8
8
  contribute?: (slot: import("@fungi.computer/shiitake").ExtensionSlotV0 | undefined) => ClientContributionV1;
9
9
  }>;