@fungi.computer/shiitake-extensions 0.2.0 → 0.4.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.
- package/README.md +33 -21
- package/dist/goal/contract.d.ts +27 -8
- package/dist/goal/contract.js +20 -3
- package/dist/goal/host.d.ts +4 -1
- package/dist/goal/host.js +375 -128
- package/dist/goal/prompts.d.ts +73 -0
- package/dist/goal/prompts.js +98 -0
- package/dist/home/host.js +9 -3
- package/dist/internal/headline.d.ts +7 -0
- package/dist/internal/headline.js +18 -0
- package/dist/loop/client.js +1 -1
- package/dist/loop/contract.d.ts +6 -2
- package/dist/loop/contract.js +10 -1
- package/dist/loop/host.js +10 -12
- package/dist/loop/prompts.d.ts +56 -0
- package/dist/loop/prompts.js +54 -0
- package/dist/question/answer-flow.d.ts +27 -0
- package/dist/question/answer-flow.js +115 -0
- package/dist/question/client.d.ts +1 -1
- package/dist/question/client.js +22 -3
- package/dist/question/contract.d.ts +202 -60
- package/dist/question/contract.js +78 -22
- package/dist/question/host.d.ts +4 -0
- package/dist/question/host.js +348 -304
- package/dist/roster/conversation-agents.d.ts +22 -15
- package/dist/roster/conversation-agents.js +161 -37
- package/dist/roster.d.ts +8 -2
- package/dist/roster.js +33 -6
- package/dist/skills/host.js +20 -2
- package/dist/subagents/client.js +64 -20
- package/dist/subagents/contract.d.ts +123 -10
- package/dist/subagents/contract.js +38 -5
- package/dist/subagents/host.d.ts +5 -5
- package/dist/subagents/host.js +152 -22
- package/docs/api/goal/contract.md +5 -2
- package/docs/api/goal/host.md +22 -1
- package/docs/api/home/host.md +6 -5
- package/docs/api/loop/contract.md +5 -2
- package/docs/api/question/client.md +2 -1
- package/docs/api/question/contract.md +183 -51
- package/docs/api/question/host.md +30 -0
- package/docs/api/roster/conversation-agents.md +15 -7
- package/docs/api/roster.md +39 -2
- package/docs/api/subagents/contract.md +101 -28
- package/docs/api/subagents/host.md +5 -2
- package/package.json +5 -6
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
import { type AgentControlDirectory } from "@fungi.computer/shiitake/agent-control";
|
|
2
2
|
import type { CapabilitySource } from "@fungi.computer/mycelium";
|
|
3
|
+
import { type ShiitakeSession } from "@fungi.computer/shiitake/client";
|
|
3
4
|
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
4
|
-
import {
|
|
5
|
+
import type { SubagentSource } from "../subagents/host.js";
|
|
5
6
|
/** Host-pack creation admission; attachments/model selection retain their own owners. */
|
|
6
7
|
export declare function assertOrdinaryAgentCreation(input: Parameters<NonNullable<AgentControlDirectory["create"]>>[0]): void;
|
|
8
|
+
/** The directory a conversation sends through; opening a recipient only needs its Sessions' extension doors. */
|
|
9
|
+
type SendingDirectory = Omit<AgentControlDirectory, "open"> & Readonly<{
|
|
10
|
+
open: (agentId: string, signal: AbortSignal) => Promise<Readonly<{
|
|
11
|
+
agent: Readonly<{
|
|
12
|
+
session: (sessionId: string) => Pick<ShiitakeSession, "extension">;
|
|
13
|
+
}>;
|
|
14
|
+
release: () => Promise<void>;
|
|
15
|
+
}>>;
|
|
16
|
+
}>;
|
|
7
17
|
/**
|
|
8
18
|
* This host's own record of the Agents each conversation created or sent
|
|
9
19
|
* accepted work to, written as its Agent-control directory accepts them
|
|
10
|
-
* (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view).
|
|
11
|
-
*
|
|
20
|
+
* (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view). Accepted
|
|
21
|
+
* child command receipts are separately reconciled against their owner's
|
|
22
|
+
* settlement records for the parent's goal guard. Neither record grants
|
|
23
|
+
* authority over a child.
|
|
12
24
|
*/
|
|
13
25
|
export declare function createConversationAgents(storage: WatchdogSqliteOwner): {
|
|
14
26
|
/** Bind acceptance recording to this conversation's existing directory. */
|
|
15
|
-
recording: (parentSessionId: string | undefined, directory:
|
|
27
|
+
recording: <Directory extends SendingDirectory>(parentSessionId: string | undefined, directory: Directory) => Directory;
|
|
16
28
|
/** Agent-control tools bound to the conversation that acquires them. */
|
|
17
29
|
control: (directoryFor: (parentSessionId: string | undefined) => AgentControlDirectory) => CapabilitySource;
|
|
18
30
|
/** The footer rows for one conversation; archived and unknown Agents leave it. */
|
|
19
|
-
source: (directory: Pick<AgentControlDirectory, "list"
|
|
31
|
+
source: (directory: Pick<AgentControlDirectory, "list"> & Pick<SubagentSource, "open">) => SubagentSource;
|
|
20
32
|
};
|
|
21
33
|
/** Both hosts compose the ordinary roster and its read-only created-Agent footer. */
|
|
22
|
-
export declare function oysterAgentExtensions(source: SubagentSource): {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
id: import("@fungi.computer/shiitake/extensions").ExtensionId;
|
|
28
|
-
capabilities: readonly import("@fungi.computer/shiitake").ExtensionCapabilityV0[];
|
|
29
|
-
grantKey?: string;
|
|
30
|
-
}>[];
|
|
31
|
-
};
|
|
34
|
+
export declare function oysterAgentExtensions(source: SubagentSource): Readonly<{
|
|
35
|
+
roster: readonly import("@fungi.computer/shiitake").RosteredExtensionV0[];
|
|
36
|
+
grants: readonly import("@fungi.computer/shiitake").ExtensionGrantV0[];
|
|
37
|
+
}>;
|
|
38
|
+
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AgentCreateRefused, createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
|
|
2
|
+
import { bindSessionExtension, } from "@fungi.computer/shiitake/client";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
2
4
|
import * as Schema from "effect/Schema";
|
|
3
|
-
import { botanicalAgentExtensions } from "../roster.js";
|
|
4
|
-
import {
|
|
5
|
+
import { botanicalAgentExtensions, botanicalExtensionsWithSubagents, } from "../roster.js";
|
|
6
|
+
import { goalContract } from "../goal/contract.js";
|
|
5
7
|
/** Host-pack creation admission; attachments/model selection retain their own owners. */
|
|
6
8
|
export function assertOrdinaryAgentCreation(input) {
|
|
7
9
|
if (input.model !== undefined)
|
|
@@ -14,11 +16,34 @@ const conversationAgentRow = Schema.Struct({
|
|
|
14
16
|
agent_id: Schema.String,
|
|
15
17
|
session_id: Schema.NullOr(Schema.String),
|
|
16
18
|
});
|
|
19
|
+
const delegatedCommandRow = Schema.Struct({
|
|
20
|
+
parent_session_id: Schema.String,
|
|
21
|
+
agent_id: Schema.String,
|
|
22
|
+
session_id: Schema.String,
|
|
23
|
+
command_id: Schema.String,
|
|
24
|
+
at: Schema.Number,
|
|
25
|
+
});
|
|
26
|
+
/** Private safety ceilings for command-status polling; not a delegation policy. */
|
|
27
|
+
const maxWatchedCommands = 64;
|
|
28
|
+
const pollMs = 2_000;
|
|
29
|
+
const maxWatchMs = 30 * 60_000;
|
|
30
|
+
/** The objective of a delegated lane: the named goal, else the send's first line. */
|
|
31
|
+
function laneObjective(text, goal) {
|
|
32
|
+
const named = goal?.trim();
|
|
33
|
+
if (named !== undefined && named !== "")
|
|
34
|
+
return named;
|
|
35
|
+
return (text
|
|
36
|
+
.split("\n")
|
|
37
|
+
.map((line) => line.trim())
|
|
38
|
+
.find((line) => line !== "") ?? text.trim()).slice(0, 4_000);
|
|
39
|
+
}
|
|
17
40
|
/**
|
|
18
41
|
* This host's own record of the Agents each conversation created or sent
|
|
19
42
|
* accepted work to, written as its Agent-control directory accepts them
|
|
20
|
-
* (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view).
|
|
21
|
-
*
|
|
43
|
+
* (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view). Accepted
|
|
44
|
+
* child command receipts are separately reconciled against their owner's
|
|
45
|
+
* settlement records for the parent's goal guard. Neither record grants
|
|
46
|
+
* authority over a child.
|
|
22
47
|
*/
|
|
23
48
|
export function createConversationAgents(storage) {
|
|
24
49
|
// Composing a host does no storage I/O; the first record or read prepares it.
|
|
@@ -27,6 +52,7 @@ export function createConversationAgents(storage) {
|
|
|
27
52
|
if (!prepared) {
|
|
28
53
|
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
54
|
storage.sql.exec("CREATE INDEX IF NOT EXISTS idx_oyster_conversation_agents_parent ON oyster_conversation_agents(parent_session_id, at)");
|
|
55
|
+
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
56
|
prepared = true;
|
|
31
57
|
}
|
|
32
58
|
return storage.sql;
|
|
@@ -36,8 +62,12 @@ export function createConversationAgents(storage) {
|
|
|
36
62
|
sql().exec("INSERT OR IGNORE INTO oyster_conversation_agents(parent_session_id, agent_id, session_id, at) VALUES (?, ?, NULL, ?)", parentSessionId, agentId, Date.now());
|
|
37
63
|
}
|
|
38
64
|
/** A send may not know its Session until the recipient accepts it. */
|
|
39
|
-
function sent(parentSessionId, agentId, sessionId) {
|
|
65
|
+
function sent(parentSessionId, agentId, sessionId, commandId) {
|
|
40
66
|
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());
|
|
67
|
+
if (sessionId !== undefined) {
|
|
68
|
+
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());
|
|
69
|
+
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);
|
|
70
|
+
}
|
|
41
71
|
}
|
|
42
72
|
/** One conversation's directory: its accepted creates and sends are recorded. */
|
|
43
73
|
function recording(parentSessionId, directory) {
|
|
@@ -57,7 +87,31 @@ export function createConversationAgents(storage) {
|
|
|
57
87
|
...recorded,
|
|
58
88
|
send: async (task, sourceSessionId, signal, invocation) => {
|
|
59
89
|
const accepted = await send(task, sourceSessionId, signal, invocation);
|
|
60
|
-
|
|
90
|
+
if (accepted.status === "accepted") {
|
|
91
|
+
const sessionId = accepted.sessionId ?? task.sessionId;
|
|
92
|
+
sent(sourceSessionId, task.agentId, sessionId, accepted.commandId);
|
|
93
|
+
// A send that opens a new Session puts it under a goal, so the
|
|
94
|
+
// lane keeps working until it reports complete or blocked. A
|
|
95
|
+
// retry replays the same request id, so the goal is set once.
|
|
96
|
+
if (task.delivery === "followUp" &&
|
|
97
|
+
task.sessionId === undefined &&
|
|
98
|
+
task.goal !== false &&
|
|
99
|
+
sessionId !== undefined) {
|
|
100
|
+
const opened = await directory.open(task.agentId, signal);
|
|
101
|
+
try {
|
|
102
|
+
await bindSessionExtension(opened.agent.session(sessionId), goalContract).call("set", {
|
|
103
|
+
objective: laneObjective(task.text, task.goal),
|
|
104
|
+
expectedRevision: null,
|
|
105
|
+
requestId: `lane-goal:${accepted.commandId}`,
|
|
106
|
+
// The send is the lane's first turn; never repeat it.
|
|
107
|
+
adoptCommandId: accepted.commandId,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await opened.release();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
61
115
|
return accepted;
|
|
62
116
|
},
|
|
63
117
|
};
|
|
@@ -75,40 +129,110 @@ export function createConversationAgents(storage) {
|
|
|
75
129
|
},
|
|
76
130
|
}),
|
|
77
131
|
/** The footer rows for one conversation; archived and unknown Agents leave it. */
|
|
78
|
-
source: (directory) =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
132
|
+
source: (directory) => {
|
|
133
|
+
const polling = new Set();
|
|
134
|
+
const signal = new AbortController().signal;
|
|
135
|
+
function poll(commandId) {
|
|
136
|
+
if (polling.has(commandId) || polling.size >= maxWatchedCommands)
|
|
137
|
+
return;
|
|
138
|
+
polling.add(commandId);
|
|
139
|
+
const check = async () => {
|
|
140
|
+
let raw;
|
|
141
|
+
try {
|
|
142
|
+
raw = sql()
|
|
143
|
+
.exec("SELECT parent_session_id, agent_id, session_id, command_id, at FROM oyster_delegated_commands WHERE command_id = ?", commandId)
|
|
144
|
+
.toArray()[0];
|
|
145
|
+
}
|
|
146
|
+
catch (cause) {
|
|
147
|
+
// The host's store was closed; no orphan poller may keep using it.
|
|
148
|
+
Effect.runFork(Effect.logError("subagents.command-store-read", cause));
|
|
149
|
+
polling.delete(commandId);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (raw === undefined) {
|
|
153
|
+
polling.delete(commandId);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const row = Schema.decodeUnknownSync(delegatedCommandRow)(raw);
|
|
157
|
+
// An unobservable command cannot justify an indefinite live wait.
|
|
158
|
+
if (Date.now() - row.at >= maxWatchMs) {
|
|
159
|
+
sql().exec("DELETE FROM oyster_delegated_commands WHERE command_id = ?", commandId);
|
|
160
|
+
polling.delete(commandId);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const opened = await directory.open(row.agent_id, signal);
|
|
165
|
+
let settled;
|
|
166
|
+
try {
|
|
167
|
+
settled = await opened.agent
|
|
168
|
+
.session(row.session_id)
|
|
169
|
+
.readSettlement(commandId, {
|
|
170
|
+
timeoutMs: 0,
|
|
171
|
+
signal,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
await opened.release();
|
|
176
|
+
}
|
|
177
|
+
if (settled !== undefined) {
|
|
178
|
+
sql().exec("DELETE FROM oyster_delegated_commands WHERE command_id = ?", commandId);
|
|
179
|
+
polling.delete(commandId);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (cause) {
|
|
184
|
+
// An unreadable child is not proof of live work; do not hold the guard.
|
|
185
|
+
Effect.runFork(Effect.logError("subagents.command-status", cause));
|
|
186
|
+
try {
|
|
187
|
+
sql().exec("DELETE FROM oyster_delegated_commands WHERE command_id = ?", commandId);
|
|
188
|
+
}
|
|
189
|
+
catch (closed) {
|
|
190
|
+
// Host teardown closed storage while the remote read was pending.
|
|
191
|
+
Effect.runFork(Effect.logError("subagents.command-store-closed", closed));
|
|
192
|
+
}
|
|
193
|
+
polling.delete(commandId);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
setTimeout(() => void check(), pollMs);
|
|
97
197
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
198
|
+
void check();
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
// Polling is demand-driven: only a parent whose goal asks about its
|
|
202
|
+
// live children watches their receipts, including ones recorded
|
|
203
|
+
// before a restart. Composing a host or sending mail does no polling.
|
|
204
|
+
hasLiveChildCommand: (sessionId) => {
|
|
205
|
+
const live = sql()
|
|
206
|
+
.exec("SELECT command_id FROM oyster_delegated_commands WHERE parent_session_id = ? AND at > ? ORDER BY at DESC LIMIT ?", sessionId, Date.now() - maxWatchMs, maxWatchedCommands)
|
|
207
|
+
.toArray()
|
|
208
|
+
.map((row) => Schema.decodeUnknownSync(Schema.Struct({ command_id: Schema.String }))(row).command_id);
|
|
209
|
+
for (const commandId of live)
|
|
210
|
+
poll(commandId);
|
|
211
|
+
return live.length > 0;
|
|
212
|
+
},
|
|
213
|
+
read: async (sessionId, limit) => {
|
|
214
|
+
const rows = sql()
|
|
215
|
+
.exec("SELECT agent_id, session_id FROM oyster_conversation_agents WHERE parent_session_id = ? ORDER BY at DESC, agent_id", sessionId)
|
|
216
|
+
.toArray()
|
|
217
|
+
.map((row) => Schema.decodeUnknownSync(conversationAgentRow)(row));
|
|
218
|
+
const known = new Map((await directory.list(new AbortController().signal)).map((agent) => [agent.agentId, agent.name]));
|
|
219
|
+
const agents = rows.flatMap((row) => {
|
|
220
|
+
const name = known.get(row.agent_id);
|
|
221
|
+
return name === undefined
|
|
222
|
+
? []
|
|
223
|
+
: [{ agentId: row.agent_id, name, sessionId: row.session_id }];
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
agents: agents.slice(0, limit),
|
|
227
|
+
more: Math.max(0, agents.length - limit),
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
open: directory.open,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
101
233
|
};
|
|
102
234
|
}
|
|
103
235
|
/** Both hosts compose the ordinary roster and its read-only created-Agent footer. */
|
|
104
236
|
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
|
-
};
|
|
237
|
+
return botanicalExtensionsWithSubagents(botanicalAgentExtensions, source);
|
|
114
238
|
}
|
package/dist/roster.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
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
|
-
* Botanical's
|
|
5
|
-
*
|
|
5
|
+
* Botanical's shared roster with explicit grants. Local composes its
|
|
6
|
+
* sender-routing question extension through `botanicalExtensionsWithSkills`;
|
|
7
|
+
* hosts without that Local capability send every Ask to a person.
|
|
6
8
|
*/
|
|
7
9
|
export declare const botanicalAgentExtensions: ExtensionRosterV0;
|
|
10
|
+
/** Compose the goal and footer over one source; an absent goal is a wiring error. */
|
|
11
|
+
export declare function botanicalExtensionsWithSubagents(base: ExtensionRosterV0, source: SubagentSource): ExtensionRosterV0;
|
|
8
12
|
/** A host with a SkillSource grants read-only skill discovery for its own runs. */
|
|
9
13
|
export declare function botanicalExtensionsWithSkills(source: SkillSource): ExtensionRosterV0;
|
|
14
|
+
/** Route Asks to the Agent that sent the work, only in a host that owns the Local answer door. */
|
|
15
|
+
export declare function botanicalExtensionsWithSenderRouting(): ExtensionRosterV0;
|
package/dist/roster.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { almanacContract } from "./almanac/contract.js";
|
|
2
2
|
import { almanacExtension } from "./almanac/host.js";
|
|
3
3
|
import { questionContract } from "./question/contract.js";
|
|
4
|
-
import { questionExtension } from "./question/host.js";
|
|
4
|
+
import { localQuestionExtension, 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
|
-
* Botanical's
|
|
14
|
-
*
|
|
14
|
+
* Botanical's shared roster with explicit grants. Local composes its
|
|
15
|
+
* sender-routing question extension through `botanicalExtensionsWithSkills`;
|
|
16
|
+
* hosts without that Local capability send every Ask to a person.
|
|
15
17
|
*/
|
|
16
18
|
export const botanicalAgentExtensions = Object.freeze({
|
|
17
19
|
roster: Object.freeze([
|
|
@@ -42,6 +44,7 @@ export const botanicalAgentExtensions = Object.freeze({
|
|
|
42
44
|
"state",
|
|
43
45
|
"settled",
|
|
44
46
|
"send",
|
|
47
|
+
"timers",
|
|
45
48
|
"commands",
|
|
46
49
|
"projection",
|
|
47
50
|
"run_tools",
|
|
@@ -72,10 +75,34 @@ export const botanicalAgentExtensions = Object.freeze({
|
|
|
72
75
|
},
|
|
73
76
|
]),
|
|
74
77
|
});
|
|
78
|
+
/** Compose the goal and footer over one source; an absent goal is a wiring error. */
|
|
79
|
+
export function botanicalExtensionsWithSubagents(base, source) {
|
|
80
|
+
if (!base.roster.some((extension) => extension.manifest.id === goalContract.id))
|
|
81
|
+
throw new Error("Cannot attach sub-agents without the goal extension");
|
|
82
|
+
return {
|
|
83
|
+
roster: [
|
|
84
|
+
...base.roster.map((extension) => extension.manifest.id === goalContract.id
|
|
85
|
+
? goalExtensionFor(source)
|
|
86
|
+
: extension),
|
|
87
|
+
subagentsExtension({ source }),
|
|
88
|
+
],
|
|
89
|
+
grants: [...base.grants, subagentsGrant],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
75
92
|
/** A host with a SkillSource grants read-only skill discovery for its own runs. */
|
|
76
93
|
export function botanicalExtensionsWithSkills(source) {
|
|
94
|
+
const local = botanicalExtensionsWithSenderRouting();
|
|
95
|
+
return {
|
|
96
|
+
roster: [...local.roster, skillsExtension({ source })],
|
|
97
|
+
grants: [...local.grants, skillsGrant],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Route Asks to the Agent that sent the work, only in a host that owns the Local answer door. */
|
|
101
|
+
export function botanicalExtensionsWithSenderRouting() {
|
|
77
102
|
return {
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
...botanicalAgentExtensions,
|
|
104
|
+
roster: botanicalAgentExtensions.roster.map((extension) => extension.manifest.id === questionContract.id
|
|
105
|
+
? localQuestionExtension
|
|
106
|
+
: extension),
|
|
80
107
|
};
|
|
81
108
|
}
|
package/dist/skills/host.js
CHANGED
|
@@ -130,11 +130,29 @@ function skillsStep(found, name) {
|
|
|
130
130
|
});
|
|
131
131
|
return ask({
|
|
132
132
|
type: "choice",
|
|
133
|
-
message:
|
|
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.
|
|
152
|
+
description: skill.description
|
|
153
|
+
.replace(/\s+/gu, " ")
|
|
154
|
+
.trim()
|
|
155
|
+
.slice(0, 100),
|
|
138
156
|
})),
|
|
139
157
|
}, {}, "name");
|
|
140
158
|
}
|
package/dist/subagents/client.js
CHANGED
|
@@ -2,32 +2,64 @@ import { defineClientEntry, } from "@fungi.computer/shiitake/client-entry";
|
|
|
2
2
|
import { subagentsContract, } from "./contract.js";
|
|
3
3
|
const presentation = {
|
|
4
4
|
working: { label: "working", tone: "running", status: "info" },
|
|
5
|
-
|
|
5
|
+
waiting_on_agent: { label: "waiting", tone: "muted", status: "neutral" },
|
|
6
|
+
needs_you: { label: "needs you", tone: "attention", status: "warning" },
|
|
6
7
|
idle: { label: "idle", tone: "settled", status: "success" },
|
|
7
8
|
failed: { label: "failed", tone: "error", status: "error" },
|
|
8
9
|
unavailable: { label: "unavailable", tone: "muted", status: "neutral" },
|
|
9
10
|
};
|
|
11
|
+
/** "waiting on Shiitake": the Agent a calm child waits on, never "you". */
|
|
12
|
+
function label(agent) {
|
|
13
|
+
return agent.state === "waiting_on_agent"
|
|
14
|
+
? `waiting on ${agent.waitingOn ?? "its parent"}`
|
|
15
|
+
: presentation[agent.state].label;
|
|
16
|
+
}
|
|
10
17
|
function row(agent) {
|
|
11
18
|
const shown = presentation[agent.state];
|
|
12
19
|
const base = {
|
|
13
20
|
id: agent.agentId,
|
|
14
|
-
title: `${agent.name} · ${
|
|
21
|
+
title: `${agent.name} · ${label(agent)}`,
|
|
15
22
|
tone: shown.tone,
|
|
16
|
-
status: { label:
|
|
23
|
+
status: { label: label(agent), tone: shown.status },
|
|
17
24
|
};
|
|
18
|
-
|
|
25
|
+
// The question itself, so a person can decide without opening the child.
|
|
26
|
+
const asked = agent.ask?.questions[0]?.question;
|
|
27
|
+
const text = asked ?? agent.attention;
|
|
28
|
+
const detailed = text === undefined ? base : { ...base, detail: text.slice(0, 200) };
|
|
19
29
|
// An Agent created but never messaged has no conversation to open yet.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
if (agent.sessionId === null)
|
|
31
|
+
return { ...detailed, detail: agent.attention ?? "not messaged yet" };
|
|
32
|
+
const opened = {
|
|
33
|
+
...detailed,
|
|
34
|
+
open: {
|
|
35
|
+
params: [
|
|
36
|
+
{ key: "agent", value: agent.agentId },
|
|
37
|
+
{ key: "session", value: agent.sessionId },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
if (agent.ask === undefined)
|
|
42
|
+
return opened;
|
|
43
|
+
const answer = {
|
|
44
|
+
id: "answerChild",
|
|
45
|
+
label: "Answer",
|
|
46
|
+
input: {
|
|
47
|
+
agentId: agent.agentId,
|
|
48
|
+
sessionId: agent.sessionId,
|
|
49
|
+
askId: agent.ask.askId,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
// A question offers the one Answer action, calm or not: a person may always
|
|
53
|
+
// answer, and the first answer wins. Only a question that needs the person
|
|
54
|
+
// takes over the composer.
|
|
55
|
+
return {
|
|
56
|
+
...opened,
|
|
57
|
+
actions: [
|
|
58
|
+
agent.state === "needs_you"
|
|
59
|
+
? { ...answer, awaiting: "question" }
|
|
60
|
+
: answer,
|
|
61
|
+
],
|
|
62
|
+
};
|
|
31
63
|
}
|
|
32
64
|
/**
|
|
33
65
|
* One summary row while the footer is collapsed: how
|
|
@@ -36,12 +68,24 @@ function row(agent) {
|
|
|
36
68
|
*/
|
|
37
69
|
function summary(view) {
|
|
38
70
|
const count = (state) => view.agents.filter((agent) => agent.state === state).length;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.map((
|
|
71
|
+
const calm = view.agents.filter((agent) => agent.state === "waiting_on_agent");
|
|
72
|
+
// Calm children name who they wait on; only needs-you children are amber.
|
|
73
|
+
const waiting = [
|
|
74
|
+
...new Set(calm.map((agent) => agent.waitingOn ?? "its parent")),
|
|
75
|
+
].map((name) => `${String(calm.filter((agent) => (agent.waitingOn ?? "its parent") === name).length)} waiting on ${name}`);
|
|
76
|
+
const counts = [
|
|
77
|
+
...["working"]
|
|
78
|
+
.map((state) => [state, count(state)])
|
|
79
|
+
.filter(([, n]) => n > 0)
|
|
80
|
+
.map(([state, n]) => `${String(n)} ${presentation[state].label}`),
|
|
81
|
+
...waiting,
|
|
82
|
+
...["needs_you", "failed", "unavailable"]
|
|
83
|
+
.map((state) => [state, count(state)])
|
|
84
|
+
.filter(([, n]) => n > 0)
|
|
85
|
+
.map(([state, n]) => `${String(n)} ${presentation[state].label}`),
|
|
86
|
+
];
|
|
43
87
|
const total = view.agents.length + view.more;
|
|
44
|
-
const tone = count("
|
|
88
|
+
const tone = count("needs_you") > 0
|
|
45
89
|
? "attention"
|
|
46
90
|
: count("failed") > 0
|
|
47
91
|
? "error"
|