@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,176 @@
1
+ import { defineExtension, } from "@fungi.computer/shiitake/extensions";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Option from "effect/Option";
4
+ import { maximumSubagents, subagentsContract, } from "./contract.js";
5
+ const defaultTiming = Object.freeze({
6
+ refreshMs: 1_000,
7
+ pollMs: 5_000,
8
+ interestMs: 30_000,
9
+ });
10
+ /** Sessions whose rows are held at once; the least recently read go first. */
11
+ const cachedSessions = 64;
12
+ /** Owner pages scanned for one child's row: 400 conversations. */
13
+ const statusPages = 2;
14
+ export const subagentsGrant = {
15
+ id: subagentsContract.id,
16
+ capabilities: ["projection"],
17
+ };
18
+ const settledState = {
19
+ running: "working",
20
+ needs_you: "waiting",
21
+ done: "idle",
22
+ errored: "failed",
23
+ };
24
+ /**
25
+ * The owner orders a page needs_you, running, then settled rows; a bounded
26
+ * scan finds the child's own row, and anything else is unavailable.
27
+ */
28
+ async function findSession(agent, sessionId) {
29
+ let after;
30
+ for (let page = 0; page < statusPages; page += 1) {
31
+ const listed = await agent.listSessions(after);
32
+ const row = listed.sessions.find((item) => item.sessionId === sessionId);
33
+ if (row !== undefined || listed.next === undefined)
34
+ return row;
35
+ after = listed.next;
36
+ }
37
+ return undefined;
38
+ }
39
+ /** Read one child's owner-decided state; a failed read is unavailable, never idle. */
40
+ async function status(source, record, signal) {
41
+ const base = {
42
+ agentId: record.agentId,
43
+ name: record.name,
44
+ sessionId: record.sessionId,
45
+ };
46
+ try {
47
+ const opened = await source.open(record.agentId, signal);
48
+ try {
49
+ if (record.sessionId === null)
50
+ return { ...base, state: "idle" };
51
+ const row = await findSession(opened.agent, record.sessionId);
52
+ if (row === undefined || row.state === "unavailable")
53
+ return { ...base, state: "unavailable" };
54
+ if (row.state !== "needs_you")
55
+ return { ...base, state: settledState[row.state] };
56
+ const snapshot = await opened.agent
57
+ .session(record.sessionId)
58
+ .read({ limit: 1 });
59
+ const attention = snapshot.attention[0]?.summary;
60
+ return attention === undefined
61
+ ? { ...base, state: "waiting" }
62
+ : { ...base, state: "waiting", attention };
63
+ }
64
+ finally {
65
+ await opened.release();
66
+ }
67
+ }
68
+ catch (cause) {
69
+ // The row shows unavailable; the operator log keeps why.
70
+ Effect.runFork(Effect.logWarning("subagents.status", cause));
71
+ return { ...base, state: "unavailable" };
72
+ }
73
+ }
74
+ const live = (value) => value?.agents.some((agent) => agent.state === "working" || agent.state === "waiting") === true;
75
+ /**
76
+ * One activation's in-memory rows. A watcher's projection read refreshes
77
+ * stale rows outside the snapshot transaction; a changed or still-live result
78
+ * publishes one observation, whose re-read keeps interest alive.
79
+ */
80
+ function createRoster(source, timing, publish) {
81
+ const entries = new Map();
82
+ const lifetime = new AbortController();
83
+ function forget(sessionId) {
84
+ clearTimeout(entries.get(sessionId)?.timer);
85
+ entries.delete(sessionId);
86
+ }
87
+ async function refresh(sessionId, entry) {
88
+ entry.refreshing = true;
89
+ clearTimeout(entry.timer);
90
+ entry.timer = undefined;
91
+ try {
92
+ const page = await source.read(sessionId, maximumSubagents - 1);
93
+ const value = {
94
+ agents: await Promise.all(page.agents
95
+ .slice(0, maximumSubagents - 1)
96
+ .map((record) => status(source, record, lifetime.signal))),
97
+ more: page.more,
98
+ };
99
+ const changed = JSON.stringify(value) !== JSON.stringify(entry.value);
100
+ entry.value = value;
101
+ if (!lifetime.signal.aborted && (changed || live(value)))
102
+ await publish(sessionId);
103
+ }
104
+ catch (cause) {
105
+ Effect.runFork(Effect.logError("subagents.refresh", cause));
106
+ }
107
+ finally {
108
+ entry.refreshedAt = Date.now();
109
+ entry.refreshing = false;
110
+ }
111
+ if (!lifetime.signal.aborted &&
112
+ entries.get(sessionId) === entry &&
113
+ live(entry.value) &&
114
+ Date.now() - entry.readAt < timing.interestMs)
115
+ entry.timer = setTimeout(() => {
116
+ void refresh(sessionId, entry);
117
+ }, timing.pollMs);
118
+ }
119
+ return {
120
+ read(sessionId) {
121
+ const now = Date.now();
122
+ let entry = entries.get(sessionId);
123
+ if (entry === undefined) {
124
+ entry = {
125
+ value: undefined,
126
+ refreshedAt: Number.NEGATIVE_INFINITY,
127
+ readAt: now,
128
+ refreshing: false,
129
+ timer: undefined,
130
+ };
131
+ entries.set(sessionId, entry);
132
+ if (entries.size > cachedSessions) {
133
+ const oldest = [...entries].sort(([, left], [, right]) => left.readAt - right.readAt)[0];
134
+ if (oldest !== undefined)
135
+ forget(oldest[0]);
136
+ }
137
+ }
138
+ entry.readAt = now;
139
+ if (!entry.refreshing && now - entry.refreshedAt >= timing.refreshMs)
140
+ void refresh(sessionId, entry);
141
+ return entry.value;
142
+ },
143
+ close() {
144
+ lifetime.abort();
145
+ for (const sessionId of entries.keys())
146
+ forget(sessionId);
147
+ },
148
+ };
149
+ }
150
+ /**
151
+ * The read-only sub-agent view: the Agents this conversation sent work to or
152
+ * created, one row each, with their live status. It owns no relationship,
153
+ * no durable state and no command; the collaboration App remains the only
154
+ * relationship owner.
155
+ */
156
+ export function subagentsExtension(options) {
157
+ const rosters = new WeakMap();
158
+ return defineExtension({ id: subagentsContract.id, version: "1.0.0" })
159
+ .config({})
160
+ .define({
161
+ hooks: {
162
+ start: (ctx) => Effect.acquireRelease(Effect.sync(() => {
163
+ const roster = createRoster(options.source, options.timing ?? defaultTiming, (sessionId) => Effect.runPromise(ctx.transact(ctx.observe(sessionId)).pipe(Effect.ignore)));
164
+ rosters.set(ctx, roster);
165
+ return roster;
166
+ }), (roster) => Effect.sync(() => {
167
+ roster.close();
168
+ rosters.delete(ctx);
169
+ })).pipe(Effect.asVoid),
170
+ },
171
+ projection: {
172
+ schema: subagentsContract.projection.schema,
173
+ read: (ctx, sessionId) => Option.fromUndefinedOr(rosters.get(ctx)?.read(sessionId)),
174
+ },
175
+ });
176
+ }
package/package.json CHANGED
@@ -1,6 +1,120 @@
1
1
  {
2
2
  "name": "@fungi.computer/shiitake-extensions",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "author": "Levi <levi@technomancy.dev>",
5
+ "private": false,
6
+ "description": "Botanical's product Shiitake extensions: goal, loop and Almanac on the generic extension surface",
7
+ "type": "module",
8
+ "exports": {
9
+ "./almanac": {
10
+ "types": "./dist/almanac/contract.d.ts",
11
+ "import": "./dist/almanac/contract.js"
12
+ },
13
+ "./almanac/host": {
14
+ "types": "./dist/almanac/host.d.ts",
15
+ "import": "./dist/almanac/host.js"
16
+ },
17
+ "./question": {
18
+ "types": "./dist/question/contract.d.ts",
19
+ "import": "./dist/question/contract.js"
20
+ },
21
+ "./question/client": {
22
+ "types": "./dist/question/client.d.ts",
23
+ "import": "./dist/question/client.js"
24
+ },
25
+ "./question/host": {
26
+ "types": "./dist/question/host.d.ts",
27
+ "import": "./dist/question/host.js"
28
+ },
29
+ "./goal": {
30
+ "types": "./dist/goal/contract.d.ts",
31
+ "import": "./dist/goal/contract.js"
32
+ },
33
+ "./goal/host": {
34
+ "types": "./dist/goal/host.d.ts",
35
+ "import": "./dist/goal/host.js"
36
+ },
37
+ "./loop": {
38
+ "types": "./dist/loop/contract.d.ts",
39
+ "import": "./dist/loop/contract.js"
40
+ },
41
+ "./loop/host": {
42
+ "types": "./dist/loop/host.d.ts",
43
+ "import": "./dist/loop/host.js"
44
+ },
45
+ "./roster": {
46
+ "types": "./dist/roster.d.ts",
47
+ "import": "./dist/roster.js"
48
+ },
49
+ "./plan": {
50
+ "types": "./dist/plan/contract.d.ts",
51
+ "import": "./dist/plan/contract.js"
52
+ },
53
+ "./plan/host": {
54
+ "types": "./dist/plan/host.d.ts",
55
+ "import": "./dist/plan/host.js"
56
+ },
57
+ "./skills/source": {
58
+ "types": "./dist/skills/source.d.ts",
59
+ "import": "./dist/skills/source.js"
60
+ },
61
+ "./skills": {
62
+ "types": "./dist/skills/host.d.ts",
63
+ "import": "./dist/skills/host.js"
64
+ },
65
+ "./home": {
66
+ "types": "./dist/home/contract.d.ts",
67
+ "import": "./dist/home/contract.js"
68
+ },
69
+ "./home/host": {
70
+ "types": "./dist/home/host.d.ts",
71
+ "import": "./dist/home/host.js"
72
+ },
73
+ "./goal/client": {
74
+ "types": "./dist/goal/client.d.ts",
75
+ "import": "./dist/goal/client.js"
76
+ },
77
+ "./loop/client": {
78
+ "types": "./dist/loop/client.d.ts",
79
+ "import": "./dist/loop/client.js"
80
+ },
81
+ "./plan/client": {
82
+ "types": "./dist/plan/client.d.ts",
83
+ "import": "./dist/plan/client.js"
84
+ },
85
+ "./subagents": {
86
+ "types": "./dist/subagents/contract.d.ts",
87
+ "import": "./dist/subagents/contract.js"
88
+ },
89
+ "./subagents/host": {
90
+ "types": "./dist/subagents/host.d.ts",
91
+ "import": "./dist/subagents/host.js"
92
+ },
93
+ "./subagents/client": {
94
+ "types": "./dist/subagents/client.d.ts",
95
+ "import": "./dist/subagents/client.js"
96
+ }
97
+ },
98
+ "files": [
99
+ "dist",
100
+ "README.md",
101
+ "LICENSE"
102
+ ],
103
+ "dependencies": {
104
+ "@fungi.computer/almanac": "0.0.1",
105
+ "@fungi.computer/cairn": "0.0.1",
106
+ "@fungi.computer/knapsack": "0.0.2",
107
+ "@fungi.computer/mycelium": "0.0.2",
108
+ "@fungi.computer/shiitake": "0.1.0",
109
+ "@fungi.computer/whistle": "0.0.1",
110
+ "effect": "4.0.2",
111
+ "papaparse": "5.7.0",
112
+ "zod": "4.4.3",
113
+ "@fungi.computer/gills": "0.0.1"
114
+ },
115
+ "license": "MIT",
116
+ "repository": {
117
+ "type": "git",
118
+ "url": "git+https://github.com/fungi-computer/shiitake-extensions.git"
119
+ }
6
120
  }