@fungi.computer/shiitake-extensions 0.3.0 → 0.5.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 +20 -16
- package/dist/goal/contract.d.ts +6 -0
- package/dist/goal/contract.js +6 -0
- package/dist/goal/host.js +183 -31
- package/dist/loop/contract.d.ts +136 -2
- package/dist/loop/contract.js +45 -2
- package/dist/loop/host.js +23 -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 -339
- package/dist/roster/conversation-agents.d.ts +21 -1
- package/dist/roster/conversation-agents.js +47 -2
- package/dist/roster.d.ts +5 -2
- package/dist/roster.js +16 -5
- package/dist/schedules.d.ts +178 -0
- package/dist/schedules.js +182 -0
- 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 +2 -4
- package/dist/subagents/host.js +152 -22
- package/docs/api/README.md +1 -0
- package/docs/api/goal/contract.md +1 -1
- package/docs/api/goal/host.md +22 -1
- package/docs/api/loop/contract.md +43 -31
- 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 +34 -8
- package/docs/api/roster.md +39 -2
- package/docs/api/schedules.md +481 -0
- package/docs/api/subagents/contract.md +101 -28
- package/docs/api/subagents/host.md +5 -3
- package/package.json +9 -6
|
@@ -1,9 +1,29 @@
|
|
|
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
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
|
+
export type SendingDirectory = Omit<AgentControlDirectory, "open"> & Readonly<{
|
|
10
|
+
/** Open the recipient's Session extension door for goal adoption. */
|
|
11
|
+
open: (agentId: string, signal: AbortSignal) => Promise<Readonly<{
|
|
12
|
+
/** Recipient Agent with a Session extension door. */
|
|
13
|
+
agent: Readonly<{
|
|
14
|
+
/** Address a recipient Session without loading its conversation. */
|
|
15
|
+
session: (sessionId: string) => Pick<ShiitakeSession, "extension">;
|
|
16
|
+
}>;
|
|
17
|
+
/** Release the acquired recipient without stopping its work. */
|
|
18
|
+
release: () => Promise<void>;
|
|
19
|
+
}>>;
|
|
20
|
+
}>;
|
|
21
|
+
/** Adopt the first accepted turn once, even when the sender was offline at delivery. */
|
|
22
|
+
export declare function adoptDelegatedGoal(session: Pick<ShiitakeSession, "extension">, input: Readonly<{
|
|
23
|
+
text: string;
|
|
24
|
+
goal: string | false | undefined;
|
|
25
|
+
commandId: string;
|
|
26
|
+
}>): Promise<void>;
|
|
7
27
|
/**
|
|
8
28
|
* This host's own record of the Agents each conversation created or sent
|
|
9
29
|
* accepted work to, written as its Agent-control directory accepts them
|
|
@@ -14,7 +34,7 @@ export declare function assertOrdinaryAgentCreation(input: Parameters<NonNullabl
|
|
|
14
34
|
*/
|
|
15
35
|
export declare function createConversationAgents(storage: WatchdogSqliteOwner): {
|
|
16
36
|
/** Bind acceptance recording to this conversation's existing directory. */
|
|
17
|
-
recording: (parentSessionId: string | undefined, directory:
|
|
37
|
+
recording: <Directory extends SendingDirectory>(parentSessionId: string | undefined, directory: Directory) => Directory;
|
|
18
38
|
/** Agent-control tools bound to the conversation that acquires them. */
|
|
19
39
|
control: (directoryFor: (parentSessionId: string | undefined) => AgentControlDirectory) => CapabilitySource;
|
|
20
40
|
/** The footer rows for one conversation; archived and unknown Agents leave it. */
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AgentCreateRefused, createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
|
|
2
|
+
import { bindSessionExtension, } from "@fungi.computer/shiitake/client";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
3
4
|
import * as Schema from "effect/Schema";
|
|
4
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)
|
|
@@ -25,6 +27,28 @@ const delegatedCommandRow = Schema.Struct({
|
|
|
25
27
|
const maxWatchedCommands = 64;
|
|
26
28
|
const pollMs = 2_000;
|
|
27
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
|
+
}
|
|
40
|
+
/** Adopt the first accepted turn once, even when the sender was offline at delivery. */
|
|
41
|
+
export async function adoptDelegatedGoal(session, input) {
|
|
42
|
+
if (input.goal === false)
|
|
43
|
+
return;
|
|
44
|
+
await bindSessionExtension(session, goalContract).call("set", {
|
|
45
|
+
objective: laneObjective(input.text, input.goal),
|
|
46
|
+
expectedRevision: null,
|
|
47
|
+
requestId: `lane-goal:${input.commandId}`,
|
|
48
|
+
// The delegated prompt is already admitted; never enqueue it twice.
|
|
49
|
+
adoptCommandId: input.commandId,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
28
52
|
/**
|
|
29
53
|
* This host's own record of the Agents each conversation created or sent
|
|
30
54
|
* accepted work to, written as its Agent-control directory accepts them
|
|
@@ -75,8 +99,29 @@ export function createConversationAgents(storage) {
|
|
|
75
99
|
...recorded,
|
|
76
100
|
send: async (task, sourceSessionId, signal, invocation) => {
|
|
77
101
|
const accepted = await send(task, sourceSessionId, signal, invocation);
|
|
78
|
-
if (accepted.status === "accepted")
|
|
79
|
-
|
|
102
|
+
if (accepted.status === "accepted") {
|
|
103
|
+
const sessionId = accepted.sessionId ?? task.sessionId;
|
|
104
|
+
sent(sourceSessionId, task.agentId, sessionId, accepted.commandId);
|
|
105
|
+
// A send that opens a new Session puts it under a goal, so the
|
|
106
|
+
// lane keeps working until it reports complete or blocked. A
|
|
107
|
+
// retry replays the same request id, so the goal is set once.
|
|
108
|
+
if (task.delivery === "followUp" &&
|
|
109
|
+
task.sessionId === undefined &&
|
|
110
|
+
task.goal !== false &&
|
|
111
|
+
sessionId !== undefined) {
|
|
112
|
+
const opened = await directory.open(task.agentId, signal);
|
|
113
|
+
try {
|
|
114
|
+
await adoptDelegatedGoal(opened.agent.session(sessionId), {
|
|
115
|
+
text: task.text,
|
|
116
|
+
goal: task.goal,
|
|
117
|
+
commandId: accepted.commandId,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await opened.release();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
80
125
|
return accepted;
|
|
81
126
|
},
|
|
82
127
|
};
|
package/dist/roster.d.ts
CHANGED
|
@@ -2,11 +2,14 @@ import type { ExtensionRosterV0 } from "@fungi.computer/shiitake/extensions";
|
|
|
2
2
|
import type { SkillSource } from "./skills/source.js";
|
|
3
3
|
import { type SubagentSource } from "./subagents/host.js";
|
|
4
4
|
/**
|
|
5
|
-
* Botanical's
|
|
6
|
-
*
|
|
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.
|
|
7
8
|
*/
|
|
8
9
|
export declare const botanicalAgentExtensions: ExtensionRosterV0;
|
|
9
10
|
/** Compose the goal and footer over one source; an absent goal is a wiring error. */
|
|
10
11
|
export declare function botanicalExtensionsWithSubagents(base: ExtensionRosterV0, source: SubagentSource): ExtensionRosterV0;
|
|
11
12
|
/** A host with a SkillSource grants read-only skill discovery for its own runs. */
|
|
12
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,7 +1,7 @@
|
|
|
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
6
|
import { goalExtension, goalExtensionFor } from "./goal/host.js";
|
|
7
7
|
import { loopContract } from "./loop/contract.js";
|
|
@@ -11,8 +11,9 @@ import { planExtension } from "./plan/host.js";
|
|
|
11
11
|
import { skillsExtension, skillsGrant } from "./skills/host.js";
|
|
12
12
|
import { subagentsExtension, subagentsGrant, } from "./subagents/host.js";
|
|
13
13
|
/**
|
|
14
|
-
* Botanical's
|
|
15
|
-
*
|
|
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.
|
|
16
17
|
*/
|
|
17
18
|
export const botanicalAgentExtensions = Object.freeze({
|
|
18
19
|
roster: Object.freeze([
|
|
@@ -90,8 +91,18 @@ export function botanicalExtensionsWithSubagents(base, source) {
|
|
|
90
91
|
}
|
|
91
92
|
/** A host with a SkillSource grants read-only skill discovery for its own runs. */
|
|
92
93
|
export function botanicalExtensionsWithSkills(source) {
|
|
94
|
+
const local = botanicalExtensionsWithSenderRouting();
|
|
93
95
|
return {
|
|
94
|
-
roster: [...
|
|
95
|
-
grants: [...
|
|
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() {
|
|
102
|
+
return {
|
|
103
|
+
...botanicalAgentExtensions,
|
|
104
|
+
roster: botanicalAgentExtensions.roster.map((extension) => extension.manifest.id === questionContract.id
|
|
105
|
+
? localQuestionExtension
|
|
106
|
+
: extension),
|
|
96
107
|
};
|
|
97
108
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { ShiitakeAgent } from "@fungi.computer/shiitake";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/** Exclusive native key within one Agent's Almanac-then-Loop traversal. */
|
|
5
|
+
export declare const agentScheduleCursorSchema: z.ZodObject<{
|
|
6
|
+
agentId: z.ZodString;
|
|
7
|
+
kind: z.ZodUnion<readonly [z.ZodLiteral<"almanac">, z.ZodLiteral<"loop">]>;
|
|
8
|
+
after: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strict>;
|
|
10
|
+
/** One bounded read; a full native page conservatively carries continuation. */
|
|
11
|
+
export declare const agentScheduleListInputSchema: z.ZodObject<{
|
|
12
|
+
after: z.ZodOptional<z.ZodObject<{
|
|
13
|
+
agentId: z.ZodString;
|
|
14
|
+
kind: z.ZodUnion<readonly [z.ZodLiteral<"almanac">, z.ZodLiteral<"loop">]>;
|
|
15
|
+
after: z.ZodOptional<z.ZodString>;
|
|
16
|
+
}, z.core.$strict>>;
|
|
17
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
}, z.core.$strict>;
|
|
19
|
+
/** Content-bearing read projection, decoded by the retained native contracts. */
|
|
20
|
+
export declare const agentScheduleRowSchema: Schema.Union<readonly [Schema.Struct<{
|
|
21
|
+
/** Agent that owns this Almanac schedule. */
|
|
22
|
+
readonly agentId: Schema.String;
|
|
23
|
+
/** Identifies the native Almanac schedule representation. */
|
|
24
|
+
readonly kind: Schema.Literal<"almanac">;
|
|
25
|
+
/** Retained Almanac row, including its instruction and lifecycle state. */
|
|
26
|
+
readonly schedule: Schema.Struct<{
|
|
27
|
+
readonly id: Schema.String;
|
|
28
|
+
readonly sessionId: Schema.String;
|
|
29
|
+
readonly prompt: Schema.String;
|
|
30
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
31
|
+
readonly expression: Schema.String;
|
|
32
|
+
readonly timeZone: Schema.String;
|
|
33
|
+
}>>;
|
|
34
|
+
readonly nextDueAt: Schema.Int;
|
|
35
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "cancelled", "finished"]>;
|
|
36
|
+
readonly lastCommandId: Schema.NullOr<Schema.String>;
|
|
37
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
38
|
+
readonly attempt: Schema.Int;
|
|
39
|
+
}>;
|
|
40
|
+
}>, Schema.Struct<{
|
|
41
|
+
/** Agent that owns this Session's Loop. */
|
|
42
|
+
readonly agentId: Schema.String;
|
|
43
|
+
/** Identifies the native Loop representation. */
|
|
44
|
+
readonly kind: Schema.Literal<"loop">;
|
|
45
|
+
/** Retained Loop row with the Session identity used to address its owner. */
|
|
46
|
+
readonly schedule: Schema.Struct<{
|
|
47
|
+
/** Session containing this exact Loop. */
|
|
48
|
+
readonly sessionId: Schema.String;
|
|
49
|
+
readonly loopId: Schema.String;
|
|
50
|
+
readonly origin: Schema.Literals<readonly ["human", "model"]>;
|
|
51
|
+
readonly expiresAt: Schema.NullOr<Schema.Int>;
|
|
52
|
+
readonly prompt: Schema.String;
|
|
53
|
+
readonly mode: Schema.Literals<readonly ["self_paced", "fixed_interval"]>;
|
|
54
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
55
|
+
readonly expression: Schema.String;
|
|
56
|
+
readonly timeZone: Schema.String;
|
|
57
|
+
}>>;
|
|
58
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "finished", "failed", "cancelled"]>;
|
|
59
|
+
readonly commandId: Schema.String;
|
|
60
|
+
readonly reason: Schema.NullOr<Schema.String>;
|
|
61
|
+
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
62
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
63
|
+
readonly consecutiveFailures: Schema.Int;
|
|
64
|
+
}>;
|
|
65
|
+
}>]>;
|
|
66
|
+
/** One Agent's retained schedule, including its native content. */
|
|
67
|
+
export type AgentScheduleRow = typeof agentScheduleRowSchema.Type;
|
|
68
|
+
/** Exact listed identity; a Loop target never selects its later replacement. */
|
|
69
|
+
export declare const agentScheduleTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
70
|
+
kind: z.ZodLiteral<"almanac">;
|
|
71
|
+
agentId: z.ZodString;
|
|
72
|
+
scheduleId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
73
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
74
|
+
kind: z.ZodLiteral<"loop">;
|
|
75
|
+
agentId: z.ZodString;
|
|
76
|
+
sessionId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
77
|
+
loopId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
78
|
+
}, z.core.$strict>], "kind">;
|
|
79
|
+
/** Identity to admit before acquiring the corresponding Agent owner. */
|
|
80
|
+
export type AgentScheduleTarget = z.output<typeof agentScheduleTargetSchema>;
|
|
81
|
+
/** A bounded content-bearing page; owner failures reject rather than look empty. */
|
|
82
|
+
export declare const agentSchedulePageSchema: Schema.Struct<{
|
|
83
|
+
/** At most 64 retained rows, ordered Almanac first and then Loop. */
|
|
84
|
+
readonly items: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
|
|
85
|
+
/** Agent that owns this Almanac schedule. */
|
|
86
|
+
readonly agentId: Schema.String;
|
|
87
|
+
/** Identifies the native Almanac schedule representation. */
|
|
88
|
+
readonly kind: Schema.Literal<"almanac">;
|
|
89
|
+
/** Retained Almanac row, including its instruction and lifecycle state. */
|
|
90
|
+
readonly schedule: Schema.Struct<{
|
|
91
|
+
readonly id: Schema.String;
|
|
92
|
+
readonly sessionId: Schema.String;
|
|
93
|
+
readonly prompt: Schema.String;
|
|
94
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
95
|
+
readonly expression: Schema.String;
|
|
96
|
+
readonly timeZone: Schema.String;
|
|
97
|
+
}>>;
|
|
98
|
+
readonly nextDueAt: Schema.Int;
|
|
99
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "cancelled", "finished"]>;
|
|
100
|
+
readonly lastCommandId: Schema.NullOr<Schema.String>;
|
|
101
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
102
|
+
readonly attempt: Schema.Int;
|
|
103
|
+
}>;
|
|
104
|
+
}>, Schema.Struct<{
|
|
105
|
+
/** Agent that owns this Session's Loop. */
|
|
106
|
+
readonly agentId: Schema.String;
|
|
107
|
+
/** Identifies the native Loop representation. */
|
|
108
|
+
readonly kind: Schema.Literal<"loop">;
|
|
109
|
+
/** Retained Loop row with the Session identity used to address its owner. */
|
|
110
|
+
readonly schedule: Schema.Struct<{
|
|
111
|
+
/** Session containing this exact Loop. */
|
|
112
|
+
readonly sessionId: Schema.String;
|
|
113
|
+
readonly loopId: Schema.String;
|
|
114
|
+
readonly origin: Schema.Literals<readonly ["human", "model"]>;
|
|
115
|
+
readonly expiresAt: Schema.NullOr<Schema.Int>;
|
|
116
|
+
readonly prompt: Schema.String;
|
|
117
|
+
readonly mode: Schema.Literals<readonly ["self_paced", "fixed_interval"]>;
|
|
118
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
119
|
+
readonly expression: Schema.String;
|
|
120
|
+
readonly timeZone: Schema.String;
|
|
121
|
+
}>>;
|
|
122
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "finished", "failed", "cancelled"]>;
|
|
123
|
+
readonly commandId: Schema.String;
|
|
124
|
+
readonly reason: Schema.NullOr<Schema.String>;
|
|
125
|
+
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
126
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
127
|
+
readonly consecutiveFailures: Schema.Int;
|
|
128
|
+
}>;
|
|
129
|
+
}>]>>;
|
|
130
|
+
/** Exclusive continuation on a full page, or null when both kinds are exhausted. */
|
|
131
|
+
readonly next: Schema.NullOr<Schema.Struct<{
|
|
132
|
+
/** Agent whose traversal the next request must continue. */
|
|
133
|
+
readonly agentId: Schema.String;
|
|
134
|
+
/** Native owner to continue within that Agent. */
|
|
135
|
+
readonly kind: Schema.Literals<readonly ["almanac", "loop"]>;
|
|
136
|
+
/** Exclusive schedule ID or Session ID; absent starts the native kind. */
|
|
137
|
+
readonly after: Schema.optionalKey<Schema.String>;
|
|
138
|
+
}>>;
|
|
139
|
+
}>;
|
|
140
|
+
/** One Agent's page, with no directory fan-out or availability policy. */
|
|
141
|
+
export type AgentSchedulePage = typeof agentSchedulePageSchema.Type;
|
|
142
|
+
/** Manage-only result: exact target and native state, never prompt or history. */
|
|
143
|
+
export declare const agentScheduleControlReceiptSchema: Schema.Union<readonly [Schema.Struct<{
|
|
144
|
+
/** Agent whose Almanac owner completed the transition. */
|
|
145
|
+
readonly agentId: Schema.String;
|
|
146
|
+
/** Identifies the controlled native Almanac schedule. */
|
|
147
|
+
readonly kind: Schema.Literal<"almanac">;
|
|
148
|
+
/** Identity of the schedule that was paused or cancelled. */
|
|
149
|
+
readonly scheduleId: Schema.String;
|
|
150
|
+
/** Native lifecycle state returned after the transition. */
|
|
151
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "cancelled", "finished"]>;
|
|
152
|
+
}>, Schema.Struct<{
|
|
153
|
+
/** Agent whose Loop owner completed the transition. */
|
|
154
|
+
readonly agentId: Schema.String;
|
|
155
|
+
/** Identifies the controlled native Loop. */
|
|
156
|
+
readonly kind: Schema.Literal<"loop">;
|
|
157
|
+
/** Session containing the controlled Loop. */
|
|
158
|
+
readonly sessionId: Schema.String;
|
|
159
|
+
/** Exact Loop identity checked by the native transition. */
|
|
160
|
+
readonly loopId: Schema.String;
|
|
161
|
+
/** Native lifecycle state returned after the transition. */
|
|
162
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "finished", "failed", "cancelled"]>;
|
|
163
|
+
}>]>;
|
|
164
|
+
/** Content-free acknowledgement of the native owner's completed transition. */
|
|
165
|
+
export type AgentScheduleControlReceipt = typeof agentScheduleControlReceiptSchema.Type;
|
|
166
|
+
/**
|
|
167
|
+
* Read one already-admitted Agent, without scanning Sessions or draining pages.
|
|
168
|
+
* The caller owns Agent, home and resource authorization and the owner's lifetime.
|
|
169
|
+
*/
|
|
170
|
+
export declare function readAgentSchedulePage(agentId: string, owner: Pick<ShiitakeAgent, "extensions">, input?: z.input<typeof agentScheduleListInputSchema>): Promise<AgentSchedulePage>;
|
|
171
|
+
/**
|
|
172
|
+
* Explicit content-returning control for callers admitted to both read and manage.
|
|
173
|
+
* In particular, private Local retains its native-row reply. The supplied owner
|
|
174
|
+
* must already be admitted for target.agentId; this helper grants no authority.
|
|
175
|
+
*/
|
|
176
|
+
export declare function controlAgentScheduleWithContent(owner: Pick<ShiitakeAgent, "extensions">, input: AgentScheduleTarget, action: "pause" | "delete"): Promise<AgentScheduleRow>;
|
|
177
|
+
/** Control an already-admitted owner; manage-only callers receive no read content. */
|
|
178
|
+
export declare function controlAgentSchedule(owner: Pick<ShiitakeAgent, "extensions">, target: AgentScheduleTarget, action: "pause" | "delete"): Promise<AgentScheduleControlReceipt>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { bindAgentExtension } from "@fungi.computer/shiitake/client";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { almanacContract, Schedule } from "./almanac/contract.js";
|
|
5
|
+
import { Loop, loopContract } from "./loop/contract.js";
|
|
6
|
+
const identity = z.string().min(1).max(256);
|
|
7
|
+
/** Exclusive native key within one Agent's Almanac-then-Loop traversal. */
|
|
8
|
+
export const agentScheduleCursorSchema = z
|
|
9
|
+
.strictObject({
|
|
10
|
+
/** Agent whose retained schedules this continuation traverses. */
|
|
11
|
+
agentId: identity,
|
|
12
|
+
/** Native owner to continue; Almanac precedes Loop in a fresh traversal. */
|
|
13
|
+
kind: z.union([z.literal("almanac"), z.literal("loop")]),
|
|
14
|
+
/** Exclusive Almanac schedule ID or Loop Session ID; absent starts the kind. */
|
|
15
|
+
after: identity.optional(),
|
|
16
|
+
})
|
|
17
|
+
.refine(({ kind, after }) => Schema.is(kind === "almanac"
|
|
18
|
+
? almanacContract.commands.list.input
|
|
19
|
+
: loopContract.commands.list.input)(after === undefined ? {} : { after }), { message: "Invalid schedule continuation key" });
|
|
20
|
+
/** One bounded read; a full native page conservatively carries continuation. */
|
|
21
|
+
export const agentScheduleListInputSchema = z.strictObject({
|
|
22
|
+
/** Native continuation from the previous page of this Agent's schedules. */
|
|
23
|
+
after: agentScheduleCursorSchema.optional(),
|
|
24
|
+
/** Shared row budget across both native kinds; defaults to and cannot exceed 64. */
|
|
25
|
+
limit: z.number().int().min(1).max(64).default(64),
|
|
26
|
+
});
|
|
27
|
+
/** Content-bearing read projection, decoded by the retained native contracts. */
|
|
28
|
+
export const agentScheduleRowSchema = Schema.Union([
|
|
29
|
+
Schema.Struct({
|
|
30
|
+
/** Agent that owns this Almanac schedule. */
|
|
31
|
+
agentId: Schema.String,
|
|
32
|
+
/** Identifies the native Almanac schedule representation. */
|
|
33
|
+
kind: Schema.Literal("almanac"),
|
|
34
|
+
/** Retained Almanac row, including its instruction and lifecycle state. */
|
|
35
|
+
schedule: Schedule,
|
|
36
|
+
}),
|
|
37
|
+
Schema.Struct({
|
|
38
|
+
/** Agent that owns this Session's Loop. */
|
|
39
|
+
agentId: Schema.String,
|
|
40
|
+
/** Identifies the native Loop representation. */
|
|
41
|
+
kind: Schema.Literal("loop"),
|
|
42
|
+
/** Retained Loop row with the Session identity used to address its owner. */
|
|
43
|
+
schedule: Schema.Struct({
|
|
44
|
+
...Loop.fields,
|
|
45
|
+
/** Session containing this exact Loop. */
|
|
46
|
+
sessionId: loopContract.commands.pauseLoop.input.fields.sessionId,
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
/** Exact listed identity; a Loop target never selects its later replacement. */
|
|
51
|
+
export const agentScheduleTargetSchema = z.discriminatedUnion("kind", [
|
|
52
|
+
z.strictObject({
|
|
53
|
+
/** Selects the native Almanac control. */
|
|
54
|
+
kind: z.literal("almanac"),
|
|
55
|
+
/** Agent whose owner the caller must admit before controlling this target. */
|
|
56
|
+
agentId: identity,
|
|
57
|
+
/** Exact native Almanac schedule to pause or cancel. */
|
|
58
|
+
scheduleId: identity.refine(Schema.is(Schedule.fields.id)),
|
|
59
|
+
}),
|
|
60
|
+
z.strictObject({
|
|
61
|
+
/** Selects an identified native Loop control. */
|
|
62
|
+
kind: z.literal("loop"),
|
|
63
|
+
/** Agent whose owner the caller must admit before controlling this target. */
|
|
64
|
+
agentId: identity,
|
|
65
|
+
/** Session containing the listed Loop. */
|
|
66
|
+
sessionId: identity.refine(Schema.is(loopContract.commands.pauseLoop.input.fields.sessionId)),
|
|
67
|
+
/** Exact listed Loop identity; a replacement must not be controlled instead. */
|
|
68
|
+
loopId: identity.refine(Schema.is(Loop.fields.loopId)),
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
/** A bounded content-bearing page; owner failures reject rather than look empty. */
|
|
72
|
+
export const agentSchedulePageSchema = Schema.Struct({
|
|
73
|
+
/** At most 64 retained rows, ordered Almanac first and then Loop. */
|
|
74
|
+
items: Schema.Array(agentScheduleRowSchema).check(Schema.isMaxLength(64)),
|
|
75
|
+
/** Exclusive continuation on a full page, or null when both kinds are exhausted. */
|
|
76
|
+
next: Schema.NullOr(Schema.Struct({
|
|
77
|
+
/** Agent whose traversal the next request must continue. */
|
|
78
|
+
agentId: Schema.String,
|
|
79
|
+
/** Native owner to continue within that Agent. */
|
|
80
|
+
kind: Schema.Literals(["almanac", "loop"]),
|
|
81
|
+
/** Exclusive schedule ID or Session ID; absent starts the native kind. */
|
|
82
|
+
after: Schema.optionalKey(Schema.String),
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
/** Manage-only result: exact target and native state, never prompt or history. */
|
|
86
|
+
export const agentScheduleControlReceiptSchema = Schema.Union([
|
|
87
|
+
Schema.Struct({
|
|
88
|
+
/** Agent whose Almanac owner completed the transition. */
|
|
89
|
+
agentId: Schema.String,
|
|
90
|
+
/** Identifies the controlled native Almanac schedule. */
|
|
91
|
+
kind: Schema.Literal("almanac"),
|
|
92
|
+
/** Identity of the schedule that was paused or cancelled. */
|
|
93
|
+
scheduleId: Schedule.fields.id,
|
|
94
|
+
/** Native lifecycle state returned after the transition. */
|
|
95
|
+
state: Schedule.fields.state,
|
|
96
|
+
}),
|
|
97
|
+
Schema.Struct({
|
|
98
|
+
/** Agent whose Loop owner completed the transition. */
|
|
99
|
+
agentId: Schema.String,
|
|
100
|
+
/** Identifies the controlled native Loop. */
|
|
101
|
+
kind: Schema.Literal("loop"),
|
|
102
|
+
/** Session containing the controlled Loop. */
|
|
103
|
+
sessionId: loopContract.commands.pauseLoop.input.fields.sessionId,
|
|
104
|
+
/** Exact Loop identity checked by the native transition. */
|
|
105
|
+
loopId: Loop.fields.loopId,
|
|
106
|
+
/** Native lifecycle state returned after the transition. */
|
|
107
|
+
state: Loop.fields.state,
|
|
108
|
+
}),
|
|
109
|
+
]);
|
|
110
|
+
/**
|
|
111
|
+
* Read one already-admitted Agent, without scanning Sessions or draining pages.
|
|
112
|
+
* The caller owns Agent, home and resource authorization and the owner's lifetime.
|
|
113
|
+
*/
|
|
114
|
+
export async function readAgentSchedulePage(agentId, owner, input = {}) {
|
|
115
|
+
const { after, limit } = agentScheduleListInputSchema.parse(input);
|
|
116
|
+
if (after !== undefined && after.agentId !== agentId)
|
|
117
|
+
throw new Error("Schedule continuation belongs to another Agent");
|
|
118
|
+
const items = [];
|
|
119
|
+
if (after?.kind !== "loop") {
|
|
120
|
+
const nativeInput = after?.after === undefined ? { limit } : { limit, after: after.after };
|
|
121
|
+
const rows = await bindAgentExtension(owner, almanacContract).call("list", nativeInput);
|
|
122
|
+
for (const schedule of rows)
|
|
123
|
+
items.push({ agentId, kind: "almanac", schedule });
|
|
124
|
+
const last = rows.at(-1);
|
|
125
|
+
if (items.length === limit && last !== undefined)
|
|
126
|
+
return {
|
|
127
|
+
items,
|
|
128
|
+
next: { agentId, kind: "almanac", after: last.id },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const remaining = limit - items.length;
|
|
132
|
+
const cursor = after?.kind === "loop" ? after.after : undefined;
|
|
133
|
+
const nativeInput = cursor === undefined
|
|
134
|
+
? { limit: remaining }
|
|
135
|
+
: { limit: remaining, after: cursor };
|
|
136
|
+
const rows = await bindAgentExtension(owner, loopContract).call("list", nativeInput);
|
|
137
|
+
for (const schedule of rows)
|
|
138
|
+
items.push({ agentId, kind: "loop", schedule });
|
|
139
|
+
const last = rows.at(-1);
|
|
140
|
+
return {
|
|
141
|
+
items,
|
|
142
|
+
next: items.length === limit && last !== undefined
|
|
143
|
+
? { agentId, kind: "loop", after: last.sessionId }
|
|
144
|
+
: null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Explicit content-returning control for callers admitted to both read and manage.
|
|
149
|
+
* In particular, private Local retains its native-row reply. The supplied owner
|
|
150
|
+
* must already be admitted for target.agentId; this helper grants no authority.
|
|
151
|
+
*/
|
|
152
|
+
export async function controlAgentScheduleWithContent(owner, input, action) {
|
|
153
|
+
const target = agentScheduleTargetSchema.parse(input);
|
|
154
|
+
if (target.kind === "almanac") {
|
|
155
|
+
const schedule = await bindAgentExtension(owner, almanacContract).call(action === "pause" ? "pause" : "cancel", { scheduleId: target.scheduleId });
|
|
156
|
+
return { agentId: target.agentId, kind: "almanac", schedule };
|
|
157
|
+
}
|
|
158
|
+
const loop = await bindAgentExtension(owner, loopContract).call(action === "pause" ? "pauseLoop" : "stopLoop", { sessionId: target.sessionId, loopId: target.loopId });
|
|
159
|
+
return {
|
|
160
|
+
agentId: target.agentId,
|
|
161
|
+
kind: "loop",
|
|
162
|
+
schedule: { ...loop, sessionId: target.sessionId },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Control an already-admitted owner; manage-only callers receive no read content. */
|
|
166
|
+
export async function controlAgentSchedule(owner, target, action) {
|
|
167
|
+
const row = await controlAgentScheduleWithContent(owner, target, action);
|
|
168
|
+
return row.kind === "almanac"
|
|
169
|
+
? {
|
|
170
|
+
agentId: row.agentId,
|
|
171
|
+
kind: "almanac",
|
|
172
|
+
scheduleId: row.schedule.id,
|
|
173
|
+
state: row.schedule.state,
|
|
174
|
+
}
|
|
175
|
+
: {
|
|
176
|
+
agentId: row.agentId,
|
|
177
|
+
kind: "loop",
|
|
178
|
+
sessionId: row.schedule.sessionId,
|
|
179
|
+
loopId: row.schedule.loopId,
|
|
180
|
+
state: row.schedule.state,
|
|
181
|
+
};
|
|
182
|
+
}
|
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"
|