@fungi.computer/shiitake-extensions 0.1.1 → 0.3.0

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.
@@ -1,7 +1,7 @@
1
1
  import { AgentCreateRefused, createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
2
+ import * as Effect from "effect/Effect";
2
3
  import * as Schema from "effect/Schema";
3
- import { botanicalAgentExtensions } from "../roster.js";
4
- import { subagentsExtension, subagentsGrant, } from "../subagents/host.js";
4
+ import { botanicalAgentExtensions, botanicalExtensionsWithSubagents, } from "../roster.js";
5
5
  /** Host-pack creation admission; attachments/model selection retain their own owners. */
6
6
  export function assertOrdinaryAgentCreation(input) {
7
7
  if (input.model !== undefined)
@@ -14,11 +14,24 @@ const conversationAgentRow = Schema.Struct({
14
14
  agent_id: Schema.String,
15
15
  session_id: Schema.NullOr(Schema.String),
16
16
  });
17
+ const delegatedCommandRow = Schema.Struct({
18
+ parent_session_id: Schema.String,
19
+ agent_id: Schema.String,
20
+ session_id: Schema.String,
21
+ command_id: Schema.String,
22
+ at: Schema.Number,
23
+ });
24
+ /** Private safety ceilings for command-status polling; not a delegation policy. */
25
+ const maxWatchedCommands = 64;
26
+ const pollMs = 2_000;
27
+ const maxWatchMs = 30 * 60_000;
17
28
  /**
18
29
  * This host's own record of the Agents each conversation created or sent
19
30
  * accepted work to, written as its Agent-control directory accepts them
20
- * (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view). It feeds
21
- * the footer only; it owns no relationship and grants nothing.
31
+ * (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view). Accepted
32
+ * child command receipts are separately reconciled against their owner's
33
+ * settlement records for the parent's goal guard. Neither record grants
34
+ * authority over a child.
22
35
  */
23
36
  export function createConversationAgents(storage) {
24
37
  // Composing a host does no storage I/O; the first record or read prepares it.
@@ -27,6 +40,7 @@ export function createConversationAgents(storage) {
27
40
  if (!prepared) {
28
41
  storage.sql.exec("CREATE TABLE IF NOT EXISTS oyster_conversation_agents (parent_session_id TEXT NOT NULL, agent_id TEXT NOT NULL, session_id TEXT, at INTEGER NOT NULL, PRIMARY KEY (parent_session_id, agent_id)) STRICT");
29
42
  storage.sql.exec("CREATE INDEX IF NOT EXISTS idx_oyster_conversation_agents_parent ON oyster_conversation_agents(parent_session_id, at)");
43
+ storage.sql.exec("CREATE TABLE IF NOT EXISTS oyster_delegated_commands (parent_session_id TEXT NOT NULL, agent_id TEXT NOT NULL, session_id TEXT NOT NULL, command_id TEXT NOT NULL PRIMARY KEY, at INTEGER NOT NULL) STRICT");
30
44
  prepared = true;
31
45
  }
32
46
  return storage.sql;
@@ -35,9 +49,13 @@ export function createConversationAgents(storage) {
35
49
  function created(parentSessionId, agentId) {
36
50
  sql().exec("INSERT OR IGNORE INTO oyster_conversation_agents(parent_session_id, agent_id, session_id, at) VALUES (?, ?, NULL, ?)", parentSessionId, agentId, Date.now());
37
51
  }
38
- /** A send names the conversation to open and moves the row to the front. */
39
- function sent(parentSessionId, agentId, sessionId) {
40
- sql().exec("INSERT INTO oyster_conversation_agents(parent_session_id, agent_id, session_id, at) VALUES (?, ?, ?, ?) ON CONFLICT(parent_session_id, agent_id) DO UPDATE SET session_id = excluded.session_id, at = excluded.at", parentSessionId, agentId, sessionId, Date.now());
52
+ /** A send may not know its Session until the recipient accepts it. */
53
+ function sent(parentSessionId, agentId, sessionId, commandId) {
54
+ sql().exec("INSERT INTO oyster_conversation_agents(parent_session_id, agent_id, session_id, at) VALUES (?, ?, ?, ?) ON CONFLICT(parent_session_id, agent_id) DO UPDATE SET session_id = excluded.session_id, at = excluded.at", parentSessionId, agentId, sessionId ?? null, Date.now());
55
+ if (sessionId !== undefined) {
56
+ sql().exec("INSERT OR REPLACE INTO oyster_delegated_commands(parent_session_id, agent_id, session_id, command_id, at) VALUES (?, ?, ?, ?, ?)", parentSessionId, agentId, sessionId, commandId, Date.now());
57
+ sql().exec("DELETE FROM oyster_delegated_commands WHERE command_id NOT IN (SELECT command_id FROM oyster_delegated_commands ORDER BY at DESC LIMIT ?)", maxWatchedCommands);
58
+ }
41
59
  }
42
60
  /** One conversation's directory: its accepted creates and sends are recorded. */
43
61
  function recording(parentSessionId, directory) {
@@ -57,7 +75,8 @@ export function createConversationAgents(storage) {
57
75
  ...recorded,
58
76
  send: async (task, sourceSessionId, signal, invocation) => {
59
77
  const accepted = await send(task, sourceSessionId, signal, invocation);
60
- sent(sourceSessionId, task.agentId, task.sessionId);
78
+ if (accepted.status === "accepted")
79
+ sent(sourceSessionId, task.agentId, accepted.sessionId ?? task.sessionId, accepted.commandId);
61
80
  return accepted;
62
81
  },
63
82
  };
@@ -75,40 +94,110 @@ export function createConversationAgents(storage) {
75
94
  },
76
95
  }),
77
96
  /** The footer rows for one conversation; archived and unknown Agents leave it. */
78
- source: (directory) => ({
79
- read: async (sessionId, limit) => {
80
- const rows = sql()
81
- .exec("SELECT agent_id, session_id FROM oyster_conversation_agents WHERE parent_session_id = ? ORDER BY at DESC, agent_id", sessionId)
82
- .toArray()
83
- .map((row) => Schema.decodeUnknownSync(conversationAgentRow)(row));
84
- const known = new Map((await directory.list(new AbortController().signal)).map((agent) => [
85
- agent.agentId,
86
- agent.name,
87
- ]));
88
- const agents = rows.flatMap((row) => {
89
- const name = known.get(row.agent_id);
90
- return name === undefined
91
- ? []
92
- : [{ agentId: row.agent_id, name, sessionId: row.session_id }];
93
- });
94
- return {
95
- agents: agents.slice(0, limit),
96
- more: Math.max(0, agents.length - limit),
97
+ source: (directory) => {
98
+ const polling = new Set();
99
+ const signal = new AbortController().signal;
100
+ function poll(commandId) {
101
+ if (polling.has(commandId) || polling.size >= maxWatchedCommands)
102
+ return;
103
+ polling.add(commandId);
104
+ const check = async () => {
105
+ let raw;
106
+ try {
107
+ raw = sql()
108
+ .exec("SELECT parent_session_id, agent_id, session_id, command_id, at FROM oyster_delegated_commands WHERE command_id = ?", commandId)
109
+ .toArray()[0];
110
+ }
111
+ catch (cause) {
112
+ // The host's store was closed; no orphan poller may keep using it.
113
+ Effect.runFork(Effect.logError("subagents.command-store-read", cause));
114
+ polling.delete(commandId);
115
+ return;
116
+ }
117
+ if (raw === undefined) {
118
+ polling.delete(commandId);
119
+ return;
120
+ }
121
+ const row = Schema.decodeUnknownSync(delegatedCommandRow)(raw);
122
+ // An unobservable command cannot justify an indefinite live wait.
123
+ if (Date.now() - row.at >= maxWatchMs) {
124
+ sql().exec("DELETE FROM oyster_delegated_commands WHERE command_id = ?", commandId);
125
+ polling.delete(commandId);
126
+ return;
127
+ }
128
+ try {
129
+ const opened = await directory.open(row.agent_id, signal);
130
+ let settled;
131
+ try {
132
+ settled = await opened.agent
133
+ .session(row.session_id)
134
+ .readSettlement(commandId, {
135
+ timeoutMs: 0,
136
+ signal,
137
+ });
138
+ }
139
+ finally {
140
+ await opened.release();
141
+ }
142
+ if (settled !== undefined) {
143
+ sql().exec("DELETE FROM oyster_delegated_commands WHERE command_id = ?", commandId);
144
+ polling.delete(commandId);
145
+ return;
146
+ }
147
+ }
148
+ catch (cause) {
149
+ // An unreadable child is not proof of live work; do not hold the guard.
150
+ Effect.runFork(Effect.logError("subagents.command-status", cause));
151
+ try {
152
+ sql().exec("DELETE FROM oyster_delegated_commands WHERE command_id = ?", commandId);
153
+ }
154
+ catch (closed) {
155
+ // Host teardown closed storage while the remote read was pending.
156
+ Effect.runFork(Effect.logError("subagents.command-store-closed", closed));
157
+ }
158
+ polling.delete(commandId);
159
+ return;
160
+ }
161
+ setTimeout(() => void check(), pollMs);
97
162
  };
98
- },
99
- open: directory.open,
100
- }),
163
+ void check();
164
+ }
165
+ return {
166
+ // Polling is demand-driven: only a parent whose goal asks about its
167
+ // live children watches their receipts, including ones recorded
168
+ // before a restart. Composing a host or sending mail does no polling.
169
+ hasLiveChildCommand: (sessionId) => {
170
+ const live = sql()
171
+ .exec("SELECT command_id FROM oyster_delegated_commands WHERE parent_session_id = ? AND at > ? ORDER BY at DESC LIMIT ?", sessionId, Date.now() - maxWatchMs, maxWatchedCommands)
172
+ .toArray()
173
+ .map((row) => Schema.decodeUnknownSync(Schema.Struct({ command_id: Schema.String }))(row).command_id);
174
+ for (const commandId of live)
175
+ poll(commandId);
176
+ return live.length > 0;
177
+ },
178
+ read: async (sessionId, limit) => {
179
+ const rows = sql()
180
+ .exec("SELECT agent_id, session_id FROM oyster_conversation_agents WHERE parent_session_id = ? ORDER BY at DESC, agent_id", sessionId)
181
+ .toArray()
182
+ .map((row) => Schema.decodeUnknownSync(conversationAgentRow)(row));
183
+ const known = new Map((await directory.list(new AbortController().signal)).map((agent) => [agent.agentId, agent.name]));
184
+ const agents = rows.flatMap((row) => {
185
+ const name = known.get(row.agent_id);
186
+ return name === undefined
187
+ ? []
188
+ : [{ agentId: row.agent_id, name, sessionId: row.session_id }];
189
+ });
190
+ return {
191
+ agents: agents.slice(0, limit),
192
+ more: Math.max(0, agents.length - limit),
193
+ };
194
+ },
195
+ open: directory.open,
196
+ };
197
+ },
101
198
  };
102
199
  }
103
200
  /** Both hosts compose the ordinary roster and its read-only created-Agent footer. */
104
201
  export function oysterAgentExtensions(source) {
105
- return {
106
- /** Ordinary extensions plus the read-only created-Agent footer. */
107
- roster: [
108
- ...botanicalAgentExtensions.roster,
109
- subagentsExtension({ source }),
110
- ],
111
- /** Host grants corresponding to the composed extension roster. */
112
- grants: [...botanicalAgentExtensions.grants, subagentsGrant],
113
- };
202
+ return botanicalExtensionsWithSubagents(botanicalAgentExtensions, source);
114
203
  }
package/dist/roster.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import type { ExtensionRosterV0 } from "@fungi.computer/shiitake/extensions";
2
2
  import type { SkillSource } from "./skills/source.js";
3
+ import { type SubagentSource } from "./subagents/host.js";
3
4
  /**
4
5
  * Botanical's product roster with explicit grants. Hub and Local pass this
5
6
  * same value; a stranger host passes its own (or an empty) roster.
6
7
  */
7
8
  export declare const botanicalAgentExtensions: ExtensionRosterV0;
9
+ /** Compose the goal and footer over one source; an absent goal is a wiring error. */
10
+ export declare function botanicalExtensionsWithSubagents(base: ExtensionRosterV0, source: SubagentSource): ExtensionRosterV0;
8
11
  /** A host with a SkillSource grants read-only skill discovery for its own runs. */
9
12
  export declare function botanicalExtensionsWithSkills(source: SkillSource): ExtensionRosterV0;
package/dist/roster.js CHANGED
@@ -3,12 +3,13 @@ import { almanacExtension } from "./almanac/host.js";
3
3
  import { questionContract } from "./question/contract.js";
4
4
  import { questionExtension } from "./question/host.js";
5
5
  import { goalContract } from "./goal/contract.js";
6
- import { goalExtension } from "./goal/host.js";
6
+ import { goalExtension, goalExtensionFor } from "./goal/host.js";
7
7
  import { loopContract } from "./loop/contract.js";
8
8
  import { loopExtension } from "./loop/host.js";
9
9
  import { planContract } from "./plan/contract.js";
10
10
  import { planExtension } from "./plan/host.js";
11
11
  import { skillsExtension, skillsGrant } from "./skills/host.js";
12
+ import { subagentsExtension, subagentsGrant, } from "./subagents/host.js";
12
13
  /**
13
14
  * Botanical's product roster with explicit grants. Hub and Local pass this
14
15
  * same value; a stranger host passes its own (or an empty) roster.
@@ -42,6 +43,7 @@ export const botanicalAgentExtensions = Object.freeze({
42
43
  "state",
43
44
  "settled",
44
45
  "send",
46
+ "timers",
45
47
  "commands",
46
48
  "projection",
47
49
  "run_tools",
@@ -72,6 +74,20 @@ export const botanicalAgentExtensions = Object.freeze({
72
74
  },
73
75
  ]),
74
76
  });
77
+ /** Compose the goal and footer over one source; an absent goal is a wiring error. */
78
+ export function botanicalExtensionsWithSubagents(base, source) {
79
+ if (!base.roster.some((extension) => extension.manifest.id === goalContract.id))
80
+ throw new Error("Cannot attach sub-agents without the goal extension");
81
+ return {
82
+ roster: [
83
+ ...base.roster.map((extension) => extension.manifest.id === goalContract.id
84
+ ? goalExtensionFor(source)
85
+ : extension),
86
+ subagentsExtension({ source }),
87
+ ],
88
+ grants: [...base.grants, subagentsGrant],
89
+ };
90
+ }
75
91
  /** A host with a SkillSource grants read-only skill discovery for its own runs. */
76
92
  export function botanicalExtensionsWithSkills(source) {
77
93
  return {
@@ -130,11 +130,29 @@ function skillsStep(found, name) {
130
130
  });
131
131
  return ask({
132
132
  type: "choice",
133
- message: `${String(found.skills.length)} ${found.skills.length === 1 ? "skill" : "skills"} discovered${skipped}`,
133
+ message: `Skills (${String(found.skills.length)})`,
134
+ summary: reasons.length === 0
135
+ ? undefined
136
+ : `${String(reasons.length)} ${reasons.length === 1 ? "skill was" : "skills were"} not loaded. Details shows why.`,
137
+ details: [
138
+ ...reasons
139
+ .slice(0, config.maximumListedDiagnostics)
140
+ .map((diagnostic) => ({
141
+ label: diagnostic.reason,
142
+ value: `${diagnostic.message} (${diagnostic.path})`,
143
+ })),
144
+ ...found.skills.map((skill) => ({
145
+ label: skill.name,
146
+ value: skill.location,
147
+ })),
148
+ ].slice(0, 24),
134
149
  options: found.skills.map((skill) => ({
135
150
  value: skill.name,
136
151
  label: skill.name,
137
- description: skill.location,
152
+ description: skill.description
153
+ .replace(/\s+/gu, " ")
154
+ .trim()
155
+ .slice(0, 100),
138
156
  })),
139
157
  }, {}, "name");
140
158
  }
@@ -9,12 +9,12 @@ export type SubagentRecord = Readonly<{
9
9
  /** A host-selected conversation; null asks the view to read its latest one. */
10
10
  sessionId: string | null;
11
11
  }>;
12
- /** The two owner reads a status needs from one child Agent. */
12
+ /** Owner reads used for status and exact delegated command settlement. */
13
13
  export type SubagentAgent = Readonly<{
14
14
  /** Read the child's owner-ordered conversation summaries. */
15
15
  listSessions: ShiitakeAgent["listSessions"];
16
16
  /** Open the read-only view of a selected child conversation. */
17
- session: (sessionId: string) => Pick<ShiitakeSession, "read">;
17
+ session: (sessionId: string) => Pick<ShiitakeSession, "read" | "readSettlement">;
18
18
  }>;
19
19
  /**
20
20
  * The host's read-only facts. `read` returns ordinary created Agents,
@@ -23,6 +23,8 @@ export type SubagentAgent = Readonly<{
23
23
  * `open` fits), and releasing it never stops the Agent's work.
24
24
  */
25
25
  export type SubagentSource = Readonly<{
26
+ /** A recorded accepted child command is still pending or running. Synchronous for settlement. */
27
+ hasLiveChildCommand: (sessionId: string) => boolean;
26
28
  /** Read a bounded list of this conversation's created Agents. */
27
29
  read: (sessionId: string, limit: number) => Promise<Readonly<{
28
30
  /** Created Agents included in this page. */
@@ -152,7 +152,10 @@ const GoalCommandInput: Union<
152
152
  confirmed: optional<Boolean>;
153
153
  }>,
154
154
  Struct<{
155
- action: Literals<readonly ["pause", "resume", "replace", "clear"]>;
155
+ action: Literals<
156
+ readonly ["pause", "resume", "replace", "clear", "clear-ask"]
157
+ >;
158
+ confirmed: optional<Boolean>;
156
159
  }>,
157
160
  Struct<{}>,
158
161
  ]
@@ -225,7 +228,7 @@ Session-owned goal controls and their visible projection.
225
228
  | <a id="property-commands"></a> `commands` | `object` | - | Goal commands admitted by the Session owner. |
226
229
  | `commands.goal` | `object` | - | Open or answer the human goal controls. |
227
230
  | `commands.goal.scope` | `"session"` | `"session"` | The Session owns this goal. |
228
- | `commands.goal.input` | `Union`\<readonly \[`Struct`\<\{ `action`: `Literal`\<`"set"`\>; `objective`: `String`; `requestId`: `String`; `expectedRevision`: `optional`\<`NullOr`\<`String`\>\>; `confirmed`: `optional`\<`Boolean`\>; \}\>, `Struct`\<\{ `action`: `Literals`\<readonly \[`"pause"`, `"resume"`, `"replace"`, `"clear"`\]\>; \}\>, `Struct`\<\{ \}\>\]\> | `GoalCommandInput` | Decode the control action. |
231
+ | `commands.goal.input` | `Union`\<readonly \[`Struct`\<\{ `action`: `Literal`\<`"set"`\>; `objective`: `String`; `requestId`: `String`; `expectedRevision`: `optional`\<`NullOr`\<`String`\>\>; `confirmed`: `optional`\<`Boolean`\>; \}\>, `Struct`\<\{ `action`: `Literals`\<readonly \[`"pause"`, `"resume"`, `"replace"`, `"clear"`, `"clear-ask"`\]\>; `confirmed`: `optional`\<`Boolean`\>; \}\>, `Struct`\<\{ \}\>\]\> | `GoalCommandInput` | Decode the control action. |
229
232
  | `commands.goal.output` | `declare`\<`WhistleCatalogStepV1`, `WhistleCatalogStepV1`\> | `CatalogStep` | Return the server-owned conversation step. |
230
233
  | `commands.pause` | `object` | - | Pause work on the current goal. |
231
234
  | `commands.pause.scope` | `"session"` | `"session"` | The Session owns this goal. |
@@ -44,11 +44,12 @@ Agent-scope choice with per-run tools and prompt over one captured table.
44
44
 
45
45
  #### Parameters
46
46
 
47
- | Parameter | Type |
48
- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
49
- | `options` | \{ `computers`: \{ `read`: (`context`) => readonly `Readonly`\<\{ `name`: `string`; `attachmentId`: `string`; `port`: `FilesPort`; \}\>[]; \}; \} |
50
- | `options.computers` | \{ `read`: (`context`) => readonly `Readonly`\<\{ `name`: `string`; `attachmentId`: `string`; `port`: `FilesPort`; \}\>[]; \} |
51
- | `options.computers.read` | (`context`) => readonly `Readonly`\<\{ `name`: `string`; `attachmentId`: `string`; `port`: `FilesPort`; \}\>[] |
47
+ | Parameter | Type | Description |
48
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
49
+ | `options` | \{ `computers`: \{ `read`: (`context`) => readonly `Readonly`\<\{ `name`: `string`; `attachmentId`: `string`; `port`: `FilesPort`; \}\>[]; `prepare?`: (`context`) => `Promise`\<`void`\>; \}; \} | - |
50
+ | `options.computers` | \{ `read`: (`context`) => readonly `Readonly`\<\{ `name`: `string`; `attachmentId`: `string`; `port`: `FilesPort`; \}\>[]; `prepare?`: (`context`) => `Promise`\<`void`\>; \} | - |
51
+ | `options.computers.read` | (`context`) => readonly `Readonly`\<\{ `name`: `string`; `attachmentId`: `string`; `port`: `FilesPort`; \}\>[] | Read the table inside the synchronous Home choice transaction. |
52
+ | `options.computers.prepare?` | (`context`) => `Promise`\<`void`\> | Optional host preparation before run-scoped Files capture. |
52
53
 
53
54
  #### Returns
54
55
 
@@ -74,7 +74,10 @@ const LoopCommandInput: Union<
74
74
  requestId: String;
75
75
  }>,
76
76
  Struct<{
77
- action: Literals<readonly ["status", "pause", "resume", "stop", "new"]>;
77
+ action: Literals<
78
+ readonly ["status", "pause", "resume", "stop", "stop-ask", "new"]
79
+ >;
80
+ confirmed: optional<Boolean>;
78
81
  }>,
79
82
  Struct<{}>,
80
83
  ]
@@ -135,7 +138,7 @@ Session-owned loop controls and their visible state.
135
138
  | <a id="property-commands"></a> `commands` | `object` | - | Loop operations admitted by the Session owner. |
136
139
  | `commands.loop` | `object` | - | Open or answer the human loop controls. |
137
140
  | `commands.loop.scope` | `"session"` | `"session"` | The Session owns this loop. |
138
- | `commands.loop.input` | `Union`\<readonly \[`Struct`\<\{ `action`: `Literal`\<`"start"`\>; `text`: `String`; `requestId`: `String`; \}\>, `Struct`\<\{ `action`: `Literals`\<readonly \[`"status"`, `"pause"`, `"resume"`, `"stop"`, `"new"`\]\>; \}\>, `Struct`\<\{ \}\>\]\> | `LoopCommandInput` | Decode the selected human action. |
141
+ | `commands.loop.input` | `Union`\<readonly \[`Struct`\<\{ `action`: `Literal`\<`"start"`\>; `text`: `String`; `requestId`: `String`; \}\>, `Struct`\<\{ `action`: `Literals`\<readonly \[`"status"`, `"pause"`, `"resume"`, `"stop"`, `"stop-ask"`, `"new"`\]\>; `confirmed`: `optional`\<`Boolean`\>; \}\>, `Struct`\<\{ \}\>\]\> | `LoopCommandInput` | Decode the selected human action. |
139
142
  | `commands.loop.output` | `declare`\<`WhistleCatalogStepV1`, `WhistleCatalogStepV1`\> | `CatalogStep` | Return the server-owned conversation step. |
140
143
  | `commands.startSelfPaced` | `object` | - | Start a loop whose next occurrence is chosen by the model. |
141
144
  | `commands.startSelfPaced.scope` | `"session"` | `"session"` | The Session owns this loop. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/shiitake-extensions",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "private": false,
6
6
  "description": "Botanical's product Shiitake extensions: goal, loop and Almanac on the generic extension surface",
@@ -108,11 +108,11 @@
108
108
  "dependencies": {
109
109
  "@fungi.computer/almanac": "0.2.0",
110
110
  "@fungi.computer/cairn": "0.1.0",
111
- "@fungi.computer/knapsack": "0.2.1",
112
- "@fungi.computer/mycelium": "0.2.1",
113
- "@fungi.computer/shiitake": "0.3.1",
111
+ "@fungi.computer/knapsack": "0.3.1",
112
+ "@fungi.computer/mycelium": "0.2.2",
113
+ "@fungi.computer/shiitake": "0.5.0",
114
114
  "@fungi.computer/watchdog": "0.1.0",
115
- "@fungi.computer/whistle": "0.1.0",
115
+ "@fungi.computer/whistle": "0.2.0",
116
116
  "effect": "4.0.2",
117
117
  "papaparse": "5.7.0",
118
118
  "zod": "4.4.3",