@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
|
@@ -2,11 +2,12 @@ import * as Schema from "effect/Schema";
|
|
|
2
2
|
/** The most Agents one conversation's footer lists; the rest are counted. */
|
|
3
3
|
export declare const maximumSubagents = 16;
|
|
4
4
|
/**
|
|
5
|
-
* What the human sees for one ordinary created Agent. `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* What the human sees for one ordinary created Agent. `waiting_on_agent` is an
|
|
6
|
+
* open question that waits on the Agent that sent the work, calm until it is
|
|
7
|
+
* escalated; `needs_you` is attention that waits on a person; `unavailable`
|
|
8
|
+
* means its status could not be read, and is never shown as idle.
|
|
8
9
|
*/
|
|
9
|
-
export declare const SubagentState: Schema.Literals<readonly ["working", "
|
|
10
|
+
export declare const SubagentState: Schema.Literals<readonly ["working", "waiting_on_agent", "needs_you", "idle", "failed", "unavailable"]>;
|
|
10
11
|
/** Display state of one created Agent's conversation. */
|
|
11
12
|
export type SubagentState = typeof SubagentState.Type;
|
|
12
13
|
/** A footer row for an ordinary Agent created by this Agent, never merely sent work. */
|
|
@@ -18,9 +19,27 @@ export declare const Subagent: Schema.Struct<{
|
|
|
18
19
|
/** Its latest conversation; null when it has none. */
|
|
19
20
|
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
20
21
|
/** Observed conversation state, including unreadable status. */
|
|
21
|
-
readonly state: Schema.Literals<readonly ["working", "
|
|
22
|
-
/** The child's own summary of what waits on
|
|
22
|
+
readonly state: Schema.Literals<readonly ["working", "waiting_on_agent", "needs_you", "idle", "failed", "unavailable"]>;
|
|
23
|
+
/** The child's own summary of what it waits on, when waiting. */
|
|
23
24
|
readonly attention: Schema.optionalKey<Schema.String>;
|
|
25
|
+
/** Display name of the Agent a `waiting_on_agent` child waits on. */
|
|
26
|
+
readonly waitingOn: Schema.optionalKey<Schema.String>;
|
|
27
|
+
/** The question a waiting child asks, so a person can answer from here. */
|
|
28
|
+
readonly ask: Schema.optionalKey<Schema.Struct<{
|
|
29
|
+
/** Durable identity of the child's open Ask. */
|
|
30
|
+
readonly askId: Schema.String;
|
|
31
|
+
/** The decisions the child asks, so they can be answered from here. */
|
|
32
|
+
readonly questions: Schema.$Array<Schema.Struct<{
|
|
33
|
+
readonly id: Schema.String;
|
|
34
|
+
readonly header: Schema.String;
|
|
35
|
+
readonly question: Schema.String;
|
|
36
|
+
readonly options: Schema.$Array<Schema.Struct<{
|
|
37
|
+
readonly label: Schema.String;
|
|
38
|
+
readonly description: Schema.String;
|
|
39
|
+
}>>;
|
|
40
|
+
readonly multiple: Schema.optionalKey<Schema.Boolean>;
|
|
41
|
+
}>>;
|
|
42
|
+
}>>;
|
|
24
43
|
}>;
|
|
25
44
|
/** Decoded footer row for a created Agent. */
|
|
26
45
|
export type Subagent = typeof Subagent.Type;
|
|
@@ -39,9 +58,27 @@ export declare const SubagentsProjection: Schema.Struct<{
|
|
|
39
58
|
/** Its latest conversation; null when it has none. */
|
|
40
59
|
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
41
60
|
/** Observed conversation state, including unreadable status. */
|
|
42
|
-
readonly state: Schema.Literals<readonly ["working", "
|
|
43
|
-
/** The child's own summary of what waits on
|
|
61
|
+
readonly state: Schema.Literals<readonly ["working", "waiting_on_agent", "needs_you", "idle", "failed", "unavailable"]>;
|
|
62
|
+
/** The child's own summary of what it waits on, when waiting. */
|
|
44
63
|
readonly attention: Schema.optionalKey<Schema.String>;
|
|
64
|
+
/** Display name of the Agent a `waiting_on_agent` child waits on. */
|
|
65
|
+
readonly waitingOn: Schema.optionalKey<Schema.String>;
|
|
66
|
+
/** The question a waiting child asks, so a person can answer from here. */
|
|
67
|
+
readonly ask: Schema.optionalKey<Schema.Struct<{
|
|
68
|
+
/** Durable identity of the child's open Ask. */
|
|
69
|
+
readonly askId: Schema.String;
|
|
70
|
+
/** The decisions the child asks, so they can be answered from here. */
|
|
71
|
+
readonly questions: Schema.$Array<Schema.Struct<{
|
|
72
|
+
readonly id: Schema.String;
|
|
73
|
+
readonly header: Schema.String;
|
|
74
|
+
readonly question: Schema.String;
|
|
75
|
+
readonly options: Schema.$Array<Schema.Struct<{
|
|
76
|
+
readonly label: Schema.String;
|
|
77
|
+
readonly description: Schema.String;
|
|
78
|
+
}>>;
|
|
79
|
+
readonly multiple: Schema.optionalKey<Schema.Boolean>;
|
|
80
|
+
}>>;
|
|
81
|
+
}>>;
|
|
45
82
|
}>>;
|
|
46
83
|
/** Additional created Agents not included in the visible rows. */
|
|
47
84
|
readonly more: Schema.Int;
|
|
@@ -50,6 +87,33 @@ export declare const SubagentsProjection: Schema.Struct<{
|
|
|
50
87
|
}>;
|
|
51
88
|
/** Decoded list and expansion state projected into the footer. */
|
|
52
89
|
export type SubagentsProjection = typeof SubagentsProjection.Type;
|
|
90
|
+
/**
|
|
91
|
+
* The question flow's own steps, each naming the child whose Ask they answer.
|
|
92
|
+
* The footer owns the conversation; the child's question owner commits it.
|
|
93
|
+
*/
|
|
94
|
+
export declare const AnswerChildInput: Schema.Union<Schema.Struct<{
|
|
95
|
+
readonly agentId: Schema.String;
|
|
96
|
+
readonly sessionId: Schema.String;
|
|
97
|
+
readonly answer: Schema.Struct<{
|
|
98
|
+
readonly selected: Schema.$Array<Schema.String>;
|
|
99
|
+
readonly text: Schema.optionalKey<Schema.String>;
|
|
100
|
+
}>;
|
|
101
|
+
readonly askId: Schema.String;
|
|
102
|
+
readonly requestId: Schema.String;
|
|
103
|
+
readonly index: Schema.Number;
|
|
104
|
+
readonly answers: Schema.$Record<Schema.String, Schema.Struct<{
|
|
105
|
+
readonly selected: Schema.$Array<Schema.String>;
|
|
106
|
+
readonly text: Schema.optionalKey<Schema.String>;
|
|
107
|
+
}>>;
|
|
108
|
+
readonly stage: Schema.Literal<"decision">;
|
|
109
|
+
} | {
|
|
110
|
+
readonly agentId: Schema.String;
|
|
111
|
+
readonly sessionId: Schema.String;
|
|
112
|
+
readonly askId: Schema.String;
|
|
113
|
+
} | {
|
|
114
|
+
readonly agentId: Schema.String;
|
|
115
|
+
readonly sessionId: Schema.String;
|
|
116
|
+
}>[]>;
|
|
53
117
|
/** Created-Agent footer control and its visible projection. */
|
|
54
118
|
export declare const subagentsContract: {
|
|
55
119
|
/** Stable extension identity. */
|
|
@@ -65,6 +129,37 @@ export declare const subagentsContract: {
|
|
|
65
129
|
/** Return the server-owned human control step. */
|
|
66
130
|
output: Schema.declare<import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1, import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1>;
|
|
67
131
|
};
|
|
132
|
+
/** Answer a question an escalated sub-agent asks, from this conversation. */
|
|
133
|
+
answerChild: {
|
|
134
|
+
/** The Session owns its footer. */
|
|
135
|
+
scope: "session";
|
|
136
|
+
/** The next step of the child's answer conversation. */
|
|
137
|
+
input: Schema.Union<Schema.Struct<{
|
|
138
|
+
readonly agentId: Schema.String;
|
|
139
|
+
readonly sessionId: Schema.String;
|
|
140
|
+
readonly answer: Schema.Struct<{
|
|
141
|
+
readonly selected: Schema.$Array<Schema.String>;
|
|
142
|
+
readonly text: Schema.optionalKey<Schema.String>;
|
|
143
|
+
}>;
|
|
144
|
+
readonly askId: Schema.String;
|
|
145
|
+
readonly requestId: Schema.String;
|
|
146
|
+
readonly index: Schema.Number;
|
|
147
|
+
readonly answers: Schema.$Record<Schema.String, Schema.Struct<{
|
|
148
|
+
readonly selected: Schema.$Array<Schema.String>;
|
|
149
|
+
readonly text: Schema.optionalKey<Schema.String>;
|
|
150
|
+
}>>;
|
|
151
|
+
readonly stage: Schema.Literal<"decision">;
|
|
152
|
+
} | {
|
|
153
|
+
readonly agentId: Schema.String;
|
|
154
|
+
readonly sessionId: Schema.String;
|
|
155
|
+
readonly askId: Schema.String;
|
|
156
|
+
} | {
|
|
157
|
+
readonly agentId: Schema.String;
|
|
158
|
+
readonly sessionId: Schema.String;
|
|
159
|
+
}>[]>;
|
|
160
|
+
/** Return the next human conversation step. */
|
|
161
|
+
output: Schema.declare<import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1, import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1>;
|
|
162
|
+
};
|
|
68
163
|
};
|
|
69
164
|
/** Footer rows and expansion state. */
|
|
70
165
|
readonly projection: {
|
|
@@ -79,9 +174,27 @@ export declare const subagentsContract: {
|
|
|
79
174
|
/** Its latest conversation; null when it has none. */
|
|
80
175
|
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
81
176
|
/** Observed conversation state, including unreadable status. */
|
|
82
|
-
readonly state: Schema.Literals<readonly ["working", "
|
|
83
|
-
/** The child's own summary of what waits on
|
|
177
|
+
readonly state: Schema.Literals<readonly ["working", "waiting_on_agent", "needs_you", "idle", "failed", "unavailable"]>;
|
|
178
|
+
/** The child's own summary of what it waits on, when waiting. */
|
|
84
179
|
readonly attention: Schema.optionalKey<Schema.String>;
|
|
180
|
+
/** Display name of the Agent a `waiting_on_agent` child waits on. */
|
|
181
|
+
readonly waitingOn: Schema.optionalKey<Schema.String>;
|
|
182
|
+
/** The question a waiting child asks, so a person can answer from here. */
|
|
183
|
+
readonly ask: Schema.optionalKey<Schema.Struct<{
|
|
184
|
+
/** Durable identity of the child's open Ask. */
|
|
185
|
+
readonly askId: Schema.String;
|
|
186
|
+
/** The decisions the child asks, so they can be answered from here. */
|
|
187
|
+
readonly questions: Schema.$Array<Schema.Struct<{
|
|
188
|
+
readonly id: Schema.String;
|
|
189
|
+
readonly header: Schema.String;
|
|
190
|
+
readonly question: Schema.String;
|
|
191
|
+
readonly options: Schema.$Array<Schema.Struct<{
|
|
192
|
+
readonly label: Schema.String;
|
|
193
|
+
readonly description: Schema.String;
|
|
194
|
+
}>>;
|
|
195
|
+
readonly multiple: Schema.optionalKey<Schema.Boolean>;
|
|
196
|
+
}>>;
|
|
197
|
+
}>>;
|
|
85
198
|
}>>;
|
|
86
199
|
/** Additional created Agents not included in the visible rows. */
|
|
87
200
|
readonly more: Schema.Int;
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { ExtensionId, } from "@fungi.computer/shiitake/extensions";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
import { CatalogStep } from "../internal/catalog.js";
|
|
4
|
+
import { AnswerFlowInput, Questions } from "../question/contract.js";
|
|
4
5
|
const identity = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
|
|
5
6
|
/** The most Agents one conversation's footer lists; the rest are counted. */
|
|
6
7
|
export const maximumSubagents = 16;
|
|
7
8
|
/**
|
|
8
|
-
* What the human sees for one ordinary created Agent. `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* What the human sees for one ordinary created Agent. `waiting_on_agent` is an
|
|
10
|
+
* open question that waits on the Agent that sent the work, calm until it is
|
|
11
|
+
* escalated; `needs_you` is attention that waits on a person; `unavailable`
|
|
12
|
+
* means its status could not be read, and is never shown as idle.
|
|
11
13
|
*/
|
|
12
14
|
export const SubagentState = Schema.Literals([
|
|
13
15
|
"working",
|
|
14
|
-
"
|
|
16
|
+
"waiting_on_agent",
|
|
17
|
+
"needs_you",
|
|
15
18
|
"idle",
|
|
16
19
|
"failed",
|
|
17
20
|
"unavailable",
|
|
@@ -26,8 +29,17 @@ export const Subagent = Schema.Struct({
|
|
|
26
29
|
sessionId: Schema.NullOr(identity),
|
|
27
30
|
/** Observed conversation state, including unreadable status. */
|
|
28
31
|
state: SubagentState,
|
|
29
|
-
/** The child's own summary of what waits on
|
|
32
|
+
/** The child's own summary of what it waits on, when waiting. */
|
|
30
33
|
attention: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(200))),
|
|
34
|
+
/** Display name of the Agent a `waiting_on_agent` child waits on. */
|
|
35
|
+
waitingOn: Schema.optionalKey(identity),
|
|
36
|
+
/** The question a waiting child asks, so a person can answer from here. */
|
|
37
|
+
ask: Schema.optionalKey(Schema.Struct({
|
|
38
|
+
/** Durable identity of the child's open Ask. */
|
|
39
|
+
askId: identity,
|
|
40
|
+
/** The decisions the child asks, so they can be answered from here. */
|
|
41
|
+
questions: Questions,
|
|
42
|
+
})),
|
|
31
43
|
});
|
|
32
44
|
/**
|
|
33
45
|
* This Agent's created Agents and how many are not listed.
|
|
@@ -42,6 +54,18 @@ export const SubagentsProjection = Schema.Struct({
|
|
|
42
54
|
/** Whether individual rows are expanded instead of summarized. */
|
|
43
55
|
expanded: Schema.Boolean,
|
|
44
56
|
});
|
|
57
|
+
/** One child, as an answer step names it beside the draft it advances. */
|
|
58
|
+
const child = {
|
|
59
|
+
/** Identity of the child Agent whose Ask is being answered. */
|
|
60
|
+
agentId: identity,
|
|
61
|
+
/** The child's conversation that holds the Ask. */
|
|
62
|
+
sessionId: identity,
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The question flow's own steps, each naming the child whose Ask they answer.
|
|
66
|
+
* The footer owns the conversation; the child's question owner commits it.
|
|
67
|
+
*/
|
|
68
|
+
export const AnswerChildInput = Schema.Union(AnswerFlowInput.members.map((member) => Schema.Struct({ ...member.fields, ...child })));
|
|
45
69
|
/** The human `/agents` command: a bare call expands or collapses the footer rows. */
|
|
46
70
|
const commands = {
|
|
47
71
|
/** Toggle the created-Agent footer between expanded and summarized views. */
|
|
@@ -53,6 +77,15 @@ const commands = {
|
|
|
53
77
|
/** Return the server-owned human control step. */
|
|
54
78
|
output: CatalogStep,
|
|
55
79
|
},
|
|
80
|
+
/** Answer a question an escalated sub-agent asks, from this conversation. */
|
|
81
|
+
answerChild: {
|
|
82
|
+
/** The Session owns its footer. */
|
|
83
|
+
scope: "session",
|
|
84
|
+
/** The next step of the child's answer conversation. */
|
|
85
|
+
input: AnswerChildInput,
|
|
86
|
+
/** Return the next human conversation step. */
|
|
87
|
+
output: CatalogStep,
|
|
88
|
+
},
|
|
56
89
|
};
|
|
57
90
|
/** Created-Agent footer control and its visible projection. */
|
|
58
91
|
export const subagentsContract = {
|
package/dist/subagents/host.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ 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" | "readSettlement">;
|
|
17
|
+
session: (sessionId: string) => Pick<ShiitakeSession, "read" | "readSettlement" | "extension">;
|
|
18
18
|
}>;
|
|
19
19
|
/**
|
|
20
20
|
* The host's read-only facts. `read` returns ordinary created Agents,
|
|
@@ -44,10 +44,8 @@ export type SubagentSource = Readonly<{
|
|
|
44
44
|
export type SubagentTiming = Readonly<{
|
|
45
45
|
/** A watcher's read refreshes rows at least this old. */
|
|
46
46
|
refreshMs: number;
|
|
47
|
-
/** While a
|
|
47
|
+
/** While a live view watches a working or waiting row, refresh this often. */
|
|
48
48
|
pollMs: number;
|
|
49
|
-
/** Polling stops once no watcher has read the Session this long. */
|
|
50
|
-
interestMs: number;
|
|
51
49
|
}>;
|
|
52
50
|
/** Host admission for the read-only created-Agent footer and human toggle. */
|
|
53
51
|
export declare const subagentsGrant: ExtensionGrantV0;
|
package/dist/subagents/host.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { defineExtension, } from "@fungi.computer/shiitake/extensions";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import * as Option from "effect/Option";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
4
5
|
import { catalog, done } from "../internal/catalog.js";
|
|
6
|
+
import { advanceAnswerFlow } from "../question/answer-flow.js";
|
|
7
|
+
import { AskProjection, questionContract } from "../question/contract.js";
|
|
5
8
|
import { maximumSubagents, subagentsContract, } from "./contract.js";
|
|
6
9
|
const defaultTiming = Object.freeze({
|
|
7
10
|
refreshMs: 1_000,
|
|
8
11
|
pollMs: 5_000,
|
|
9
|
-
interestMs: 30_000,
|
|
10
12
|
});
|
|
11
|
-
/** Sessions
|
|
13
|
+
/** Sessions held at once; evict unwatched rows first, then least recently read. */
|
|
12
14
|
const cachedSessions = 64;
|
|
13
15
|
/** Owner pages scanned for one child's row: 400 conversations. */
|
|
14
16
|
const statusPages = 2;
|
|
@@ -19,7 +21,7 @@ export const subagentsGrant = {
|
|
|
19
21
|
};
|
|
20
22
|
const settledState = {
|
|
21
23
|
running: "working",
|
|
22
|
-
needs_you: "
|
|
24
|
+
needs_you: "needs_you",
|
|
23
25
|
done: "idle",
|
|
24
26
|
errored: "failed",
|
|
25
27
|
};
|
|
@@ -56,15 +58,44 @@ async function status(source, record, signal) {
|
|
|
56
58
|
if (row === undefined || row.state === "unavailable")
|
|
57
59
|
return { ...base, state: "unavailable" };
|
|
58
60
|
const selected = { ...base, sessionId: row.sessionId };
|
|
59
|
-
if (row.state
|
|
61
|
+
if (row.state === "running" || row.state === "errored")
|
|
60
62
|
return { ...selected, state: settledState[row.state] };
|
|
63
|
+
// An item waiting on an Agent is calm and leaves the inbox state alone,
|
|
64
|
+
// so a finished child is read for it too. Escalated items wait on a person.
|
|
61
65
|
const snapshot = await opened.agent
|
|
62
66
|
.session(row.sessionId)
|
|
63
67
|
.read({ limit: 1 });
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
?
|
|
67
|
-
:
|
|
68
|
+
const slot = snapshot.extensions[questionContract.id];
|
|
69
|
+
const asked = slot?.status === "running"
|
|
70
|
+
? Schema.decodeUnknownOption(AskProjection)(slot.value)
|
|
71
|
+
: Option.none();
|
|
72
|
+
const openAsks = Option.isSome(asked) ? asked.value.asks : [];
|
|
73
|
+
const routed = snapshot.attention.find((item) => item.waitingOn);
|
|
74
|
+
if (routed?.waitingOn !== undefined) {
|
|
75
|
+
const waiting = {
|
|
76
|
+
...selected,
|
|
77
|
+
state: "waiting_on_agent",
|
|
78
|
+
attention: routed.summary,
|
|
79
|
+
waitingOn: routed.waitingOn.name,
|
|
80
|
+
};
|
|
81
|
+
// A person may always answer first; the calm row offers it too.
|
|
82
|
+
const routedAsk = openAsks.find((open) => open.askId === routed.id);
|
|
83
|
+
return routedAsk === undefined
|
|
84
|
+
? waiting
|
|
85
|
+
: {
|
|
86
|
+
...waiting,
|
|
87
|
+
ask: { askId: routedAsk.askId, questions: routedAsk.questions },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const attention = snapshot.attention.find((item) => item.inbox !== false)?.summary;
|
|
91
|
+
if (row.state === "done")
|
|
92
|
+
return { ...selected, state: "idle" };
|
|
93
|
+
const ask = openAsks.find((open) => open.waitingOn === undefined);
|
|
94
|
+
const needing = { ...selected, state: "needs_you" };
|
|
95
|
+
const summary = attention === undefined ? needing : { ...needing, attention };
|
|
96
|
+
return ask === undefined
|
|
97
|
+
? summary
|
|
98
|
+
: { ...summary, ask: { askId: ask.askId, questions: ask.questions } };
|
|
68
99
|
}
|
|
69
100
|
finally {
|
|
70
101
|
await opened.release();
|
|
@@ -76,19 +107,34 @@ async function status(source, record, signal) {
|
|
|
76
107
|
return { ...base, state: "unavailable" };
|
|
77
108
|
}
|
|
78
109
|
}
|
|
79
|
-
const live = (value) => value?.agents.some((agent) => agent.state === "working" ||
|
|
110
|
+
const live = (value) => value?.agents.some((agent) => agent.state === "working" ||
|
|
111
|
+
agent.state === "waiting_on_agent" ||
|
|
112
|
+
agent.state === "needs_you") === true;
|
|
80
113
|
/**
|
|
81
114
|
* One activation's in-memory rows. A watcher's projection read refreshes
|
|
82
|
-
* stale rows outside the snapshot transaction
|
|
83
|
-
*
|
|
115
|
+
* stale rows outside the snapshot transaction. Only changed rows publish;
|
|
116
|
+
* only an actual live-view registration keeps polling alive.
|
|
84
117
|
*/
|
|
85
|
-
function createRoster(source, timing, publish) {
|
|
118
|
+
function createRoster(source, timing, publish, isObserved) {
|
|
86
119
|
const entries = new Map();
|
|
87
120
|
const lifetime = new AbortController();
|
|
88
121
|
function forget(sessionId) {
|
|
89
122
|
clearTimeout(entries.get(sessionId)?.timer);
|
|
90
123
|
entries.delete(sessionId);
|
|
91
124
|
}
|
|
125
|
+
function schedule(sessionId, entry) {
|
|
126
|
+
if (!lifetime.signal.aborted &&
|
|
127
|
+
entries.get(sessionId) === entry &&
|
|
128
|
+
entry.timer === undefined &&
|
|
129
|
+
!entry.refreshing &&
|
|
130
|
+
live(entry.value) &&
|
|
131
|
+
isObserved(sessionId))
|
|
132
|
+
entry.timer = setTimeout(() => {
|
|
133
|
+
entry.timer = undefined;
|
|
134
|
+
if (isObserved(sessionId))
|
|
135
|
+
void refresh(sessionId, entry);
|
|
136
|
+
}, timing.pollMs);
|
|
137
|
+
}
|
|
92
138
|
async function refresh(sessionId, entry) {
|
|
93
139
|
entry.refreshing = true;
|
|
94
140
|
clearTimeout(entry.timer);
|
|
@@ -103,7 +149,7 @@ function createRoster(source, timing, publish) {
|
|
|
103
149
|
};
|
|
104
150
|
const changed = JSON.stringify(value) !== JSON.stringify(entry.value);
|
|
105
151
|
entry.value = value;
|
|
106
|
-
if (!lifetime.signal.aborted &&
|
|
152
|
+
if (!lifetime.signal.aborted && changed)
|
|
107
153
|
await publish(sessionId);
|
|
108
154
|
}
|
|
109
155
|
catch (cause) {
|
|
@@ -113,13 +159,7 @@ function createRoster(source, timing, publish) {
|
|
|
113
159
|
entry.refreshedAt = Date.now();
|
|
114
160
|
entry.refreshing = false;
|
|
115
161
|
}
|
|
116
|
-
|
|
117
|
-
entries.get(sessionId) === entry &&
|
|
118
|
-
live(entry.value) &&
|
|
119
|
-
Date.now() - entry.readAt < timing.interestMs)
|
|
120
|
-
entry.timer = setTimeout(() => {
|
|
121
|
-
void refresh(sessionId, entry);
|
|
122
|
-
}, timing.pollMs);
|
|
162
|
+
schedule(sessionId, entry);
|
|
123
163
|
}
|
|
124
164
|
return {
|
|
125
165
|
/** Flip whether the footer lists the rows; undefined while there is nothing to list. */
|
|
@@ -144,7 +184,8 @@ function createRoster(source, timing, publish) {
|
|
|
144
184
|
};
|
|
145
185
|
entries.set(sessionId, entry);
|
|
146
186
|
if (entries.size > cachedSessions) {
|
|
147
|
-
const oldest = [...entries].sort(([, left], [, right]) =>
|
|
187
|
+
const oldest = [...entries].sort(([leftId, left], [rightId, right]) => Number(isObserved(leftId)) - Number(isObserved(rightId)) ||
|
|
188
|
+
left.readAt - right.readAt)[0];
|
|
148
189
|
if (oldest !== undefined)
|
|
149
190
|
forget(oldest[0]);
|
|
150
191
|
}
|
|
@@ -152,10 +193,46 @@ function createRoster(source, timing, publish) {
|
|
|
152
193
|
entry.readAt = now;
|
|
153
194
|
if (!entry.refreshing && now - entry.refreshedAt >= timing.refreshMs)
|
|
154
195
|
void refresh(sessionId, entry);
|
|
196
|
+
else
|
|
197
|
+
schedule(sessionId, entry);
|
|
155
198
|
return entry.value === undefined
|
|
156
199
|
? undefined
|
|
157
200
|
: { ...entry.value, expanded: entry.expanded };
|
|
158
201
|
},
|
|
202
|
+
/** The last read of one child of this conversation, if it is listed. */
|
|
203
|
+
child(sessionId, agentId, childSessionId) {
|
|
204
|
+
return entries
|
|
205
|
+
.get(sessionId)
|
|
206
|
+
?.value?.agents.find((agent) => agent.agentId === agentId && agent.sessionId === childSessionId);
|
|
207
|
+
},
|
|
208
|
+
/**
|
|
209
|
+
* Hand a person's completed answer to the child's own question owner. The
|
|
210
|
+
* command that asked returns first, so the answer is sent outside it; a
|
|
211
|
+
* failure is logged and the row stays, so the person can answer again.
|
|
212
|
+
*/
|
|
213
|
+
submit(sessionId, target) {
|
|
214
|
+
void (async () => {
|
|
215
|
+
const opened = await source.open(target.agentId, lifetime.signal);
|
|
216
|
+
try {
|
|
217
|
+
await opened.agent
|
|
218
|
+
.session(target.sessionId)
|
|
219
|
+
.extension(questionContract.id)
|
|
220
|
+
.call("answer", {
|
|
221
|
+
askId: target.askId,
|
|
222
|
+
requestId: target.requestId,
|
|
223
|
+
answers: target.answers,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
await opened.release();
|
|
228
|
+
}
|
|
229
|
+
const entry = entries.get(sessionId);
|
|
230
|
+
if (entry !== undefined && !entry.refreshing)
|
|
231
|
+
await refresh(sessionId, entry);
|
|
232
|
+
})().catch((cause) => {
|
|
233
|
+
Effect.runFork(Effect.logError("subagents.answer", cause));
|
|
234
|
+
});
|
|
235
|
+
},
|
|
159
236
|
close() {
|
|
160
237
|
lifetime.abort();
|
|
161
238
|
for (const sessionId of entries.keys())
|
|
@@ -177,7 +254,7 @@ export function subagentsExtension(options) {
|
|
|
177
254
|
.define({
|
|
178
255
|
hooks: {
|
|
179
256
|
start: (ctx) => Effect.acquireRelease(Effect.sync(() => {
|
|
180
|
-
const roster = createRoster(options.source, options.timing ?? defaultTiming, (sessionId) => Effect.runPromise(ctx.transact(ctx.observe(sessionId)).pipe(Effect.ignore)));
|
|
257
|
+
const roster = createRoster(options.source, options.timing ?? defaultTiming, (sessionId) => Effect.runPromise(ctx.transact(ctx.observe(sessionId)).pipe(Effect.ignore)), ctx.isObserved);
|
|
181
258
|
rosters.set(ctx, roster);
|
|
182
259
|
return roster;
|
|
183
260
|
}), (roster) => Effect.sync(() => {
|
|
@@ -197,6 +274,13 @@ export function subagentsExtension(options) {
|
|
|
197
274
|
slash: { aliases: ["agents"], order: 6 },
|
|
198
275
|
},
|
|
199
276
|
},
|
|
277
|
+
{
|
|
278
|
+
// Reached only from a sub-agent row's Answer action.
|
|
279
|
+
id: "answerChild",
|
|
280
|
+
title: "Answer a sub-agent",
|
|
281
|
+
category: "Conversation",
|
|
282
|
+
description: "Answer a question a sub-agent has escalated to you",
|
|
283
|
+
},
|
|
200
284
|
]),
|
|
201
285
|
projection: {
|
|
202
286
|
schema: subagentsContract.projection.schema,
|
|
@@ -216,6 +300,52 @@ export function subagentsExtension(options) {
|
|
|
216
300
|
// The changed rows already show the result; don't echo the toggle.
|
|
217
301
|
return { type: "done" };
|
|
218
302
|
}),
|
|
303
|
+
answerChild: ({ sessionId, input }) => Effect.gen(function* () {
|
|
304
|
+
const roster = rosters.get(ctx);
|
|
305
|
+
const row = roster?.child(sessionId, input.agentId, input.sessionId);
|
|
306
|
+
if (roster === undefined || row?.ask === undefined)
|
|
307
|
+
return done({
|
|
308
|
+
type: "info",
|
|
309
|
+
message: "That question is no longer waiting.",
|
|
310
|
+
});
|
|
311
|
+
const outcome = yield* advanceAnswerFlow({ ...row.ask, closed: false }, input);
|
|
312
|
+
if (outcome.type === "complete") {
|
|
313
|
+
roster.submit(sessionId, {
|
|
314
|
+
agentId: input.agentId,
|
|
315
|
+
sessionId: input.sessionId,
|
|
316
|
+
askId: outcome.askId,
|
|
317
|
+
requestId: outcome.requestId,
|
|
318
|
+
answers: outcome.answers,
|
|
319
|
+
});
|
|
320
|
+
return done({
|
|
321
|
+
type: "success",
|
|
322
|
+
// The child's owner commits it; an answer that loses the race changes nothing.
|
|
323
|
+
message: `Sending your answer to ${row.name}.`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const step = outcome.step;
|
|
327
|
+
// Each prompt continues here, naming the child it is for, and
|
|
328
|
+
// says whose question it is: the composer shows only the prompt.
|
|
329
|
+
return step.type === "prompt"
|
|
330
|
+
? {
|
|
331
|
+
...step,
|
|
332
|
+
prompt: {
|
|
333
|
+
...step.prompt,
|
|
334
|
+
summary: step.prompt.summary === undefined
|
|
335
|
+
? `${row.name} asks`
|
|
336
|
+
: `${row.name} asks · ${step.prompt.summary}`,
|
|
337
|
+
},
|
|
338
|
+
next: {
|
|
339
|
+
...step.next,
|
|
340
|
+
arguments: {
|
|
341
|
+
...step.next.arguments,
|
|
342
|
+
agentId: input.agentId,
|
|
343
|
+
sessionId: input.sessionId,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
: step;
|
|
348
|
+
}),
|
|
219
349
|
}),
|
|
220
350
|
},
|
|
221
351
|
});
|
package/docs/api/README.md
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
| [question/host](question/host.md) | - |
|
|
29
29
|
| [roster](roster.md) | - |
|
|
30
30
|
| [roster/conversation-agents](roster/conversation-agents.md) | - |
|
|
31
|
+
| [schedules](schedules.md) | - |
|
|
31
32
|
| [skills/host](skills/host.md) | - |
|
|
32
33
|
| [skills/source](skills/source.md) | - |
|
|
33
34
|
| [subagents/client](subagents/client.md) | - |
|
|
@@ -244,7 +244,7 @@ Session-owned goal controls and their visible projection.
|
|
|
244
244
|
| `commands.clear.output` | `Struct`\<\{ \}\> | - | A successful clear has no result fields. |
|
|
245
245
|
| `commands.set` | `object` | - | Set an objective against the revision the person saw. |
|
|
246
246
|
| `commands.set.scope` | `"session"` | `"session"` | The Session owns this goal. |
|
|
247
|
-
| `commands.set.input` | `Struct`\<\{ `objective`: `String`; `expectedRevision`: `NullOr`\<`String`\>; `requestId`: `String`; \}\>
|
|
247
|
+
| `commands.set.input` | `Struct`\<\{ `objective`: `String`; `expectedRevision`: `NullOr`\<`String`\>; `requestId`: `String`; `adoptCommandId`: `optionalKey`\<`String`\>; \}\> | - | Decode the new objective and replacement fence. |
|
|
248
248
|
| `commands.set.output` | `Struct`\<\{ `goal`: `Struct`\<\{ `revision`: `String`; `objective`: `String`; `state`: `Literals`\<readonly \[`"active"`, `"paused"`\]\>; `startedAt`: `Int`; `pauseCause`: `NullOr`\<`Struct`\<\{ `kind`: `Literals`\<...\>; `detail`: `NullOr`\<...\>; \}\>\>; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | - | Return the goal and its admission receipt. |
|
|
249
249
|
| <a id="property-projection"></a> `projection` | `object` | - | Current and ended goals visible to clients. |
|
|
250
250
|
| `projection.schema` | `Struct`\<\{ `goal`: `NullOr`\<`Struct`\<\{ `revision`: `String`; `objective`: `String`; `state`: `Literals`\<readonly \[`"active"`, `"paused"`\]\>; `startedAt`: `Int`; `pauseCause`: `NullOr`\<`Struct`\<\{ `kind`: `Literals`\<...\>; `detail`: `NullOr`\<...\>; \}\>\>; \}\>\>; `ended`: `$Array`\<`Struct`\<\{ `sequence`: `Int`; `objective`: `String`; `outcome`: `Literals`\<readonly \[`"achieved"`, `"cleared"`\]\>; `reason`: `NullOr`\<`String`\>; `endedAt`: `Int`; `afterSequence`: `Int`; \}\>\>; `omitted`: `Int`; \}\> | `GoalProjection` | Decoder for the goal projection. |
|
package/docs/api/goal/host.md
CHANGED
|
@@ -26,4 +26,25 @@ The commands this extension advertises to every client.
|
|
|
26
26
|
const goalExtension: RosteredExtensionV0;
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Ordinary roster when no created-Agent directory is available.
|
|
30
|
+
|
|
31
|
+
## Functions
|
|
32
|
+
|
|
33
|
+
### goalExtensionFor()
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
function goalExtensionFor(subagents?): RosteredExtensionV0;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Botanical's Session goal; hosts with created-Agent facts include them in
|
|
40
|
+
progress.
|
|
41
|
+
|
|
42
|
+
#### Parameters
|
|
43
|
+
|
|
44
|
+
| Parameter | Type |
|
|
45
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| `subagents?` | `Readonly`\<\{ `hasLiveChildCommand`: (`sessionId`) => `boolean`; `read`: (`sessionId`, `limit`) => `Promise`\<`Readonly`\<\{ `agents`: readonly `Readonly`\<\{ `agentId`: `string`; `name`: `string`; `sessionId`: ... \| ...; \}\>[]; `more`: `number`; \}\>\>; `open`: (`agentId`, `signal`) => `Promise`\<`Readonly`\<\{ `agent`: [`SubagentAgent`](../subagents/host.md#subagentagent); `release`: () => `Promise`\<`void`\>; \}\>\>; \}\> |
|
|
47
|
+
|
|
48
|
+
#### Returns
|
|
49
|
+
|
|
50
|
+
`RosteredExtensionV0`
|