@fungi.computer/shiitake-extensions 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -18
- package/dist/goal/contract.d.ts +21 -8
- package/dist/goal/contract.js +14 -3
- package/dist/goal/host.d.ts +4 -1
- package/dist/goal/host.js +203 -106
- package/dist/goal/prompts.d.ts +73 -0
- package/dist/goal/prompts.js +98 -0
- package/dist/home/host.d.ts +5 -0
- package/dist/home/host.js +11 -4
- 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/host.js +37 -2
- package/dist/roster/conversation-agents.d.ts +10 -14
- package/dist/roster/conversation-agents.js +128 -39
- package/dist/roster.d.ts +3 -0
- package/dist/roster.js +17 -1
- package/dist/skills/host.js +20 -2
- package/dist/subagents/host.d.ts +4 -2
- package/docs/api/goal/contract.md +5 -2
- package/docs/api/home/host.md +6 -5
- package/docs/api/loop/contract.md +5 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -65,16 +65,16 @@ helper borrows the Agent. Its owner closes it when the host's lifetime ends.
|
|
|
65
65
|
|
|
66
66
|
## Core concepts
|
|
67
67
|
|
|
68
|
-
| Extension | Ownership and behavior
|
|
69
|
-
| --------- |
|
|
70
|
-
| Ask | Up to four open decisions per Session. The model asks or withdraws; humans answer or decline. A live waiter receives the answer, while a parked question resumes through one follow-up.
|
|
71
|
-
| Goal | One live or paused objective per Session. The model creates, reads and completes it through `goal.*` tools; humans set, pause, resume or clear it. Esc, failed work, a block or three tool-less continuations pause it. Closing the app pauses it only if work stops without resuming; background work and runtime-auto-resumed turns keep it live. An achieved or cleared goal becomes one chat line in its history.
|
|
72
|
-
| Loop | One repeated prompt per Session. It runs on a fixed cron cadence or lets its own active run request the next occurrence. Commands start, pause, resume and stop it.
|
|
73
|
-
| Plan | One checklist per Session. `plan.update_plan` replaces it whole, with at most one step in progress and at most 50 steps; `steps: []` clears it. The human clears it with `/plan clear` or the footer row's Clear plan action, through the same owner. `/plan` opens the full checklist in the footer; updates keep only the current step and progress visible, and a finished or cleared plan leaves the footer while its history stays in state.
|
|
74
|
-
| Almanac | Agent-scope one-shot or recurring schedules. Each schedule targets a Session and tracks admission, retry and its next due time.
|
|
75
|
-
| Skills | Host-supplied, read-only skill discovery. Metadata enters the prompt; `skill.load` reads a skill body or supporting file; `/skills` lists them.
|
|
76
|
-
| Home | Agent-scope choice of an attached Computer. Each run captures its scoped file tools and current attachment choice.
|
|
77
|
-
| Subagents | A read-only footer view: one row per Agent this Session sent work to or created, with its live status (working, waiting on you, idle, failed, unavailable). A row opens that Agent's conversation, and `/agents` collapses the rows to one summary row and expands them again. It owns no relationship and no durable state.
|
|
68
|
+
| Extension | Ownership and behavior |
|
|
69
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| Ask | Up to four open decisions per Session. The model asks or withdraws; humans answer or decline. A live waiter receives the answer, while a parked question resumes through one follow-up. |
|
|
71
|
+
| Goal | One live or paused objective per Session. The model creates, reads and completes it through `goal.*` tools, from any later turn of its Session; humans set, pause, resume or clear it. Esc, failed work, a block or three tool-less continuations pause it; a tool-less goal turn while a sub-agent command is live waits instead. Closing the app pauses it only if work stops without resuming; background work and runtime-auto-resumed turns keep it live. An achieved or cleared goal becomes one chat line in its history. |
|
|
72
|
+
| Loop | One repeated prompt per Session. It runs on a fixed cron cadence or lets its own active run request the next occurrence. Commands start, pause, resume and stop it. |
|
|
73
|
+
| Plan | One checklist per Session. `plan.update_plan` replaces it whole, with at most one step in progress and at most 50 steps; `steps: []` clears it. The human clears it with `/plan clear` or the footer row's Clear plan action, through the same owner. `/plan` opens the full checklist in the footer; updates keep only the current step and progress visible, and a finished or cleared plan leaves the footer while its history stays in state. |
|
|
74
|
+
| Almanac | Agent-scope one-shot or recurring schedules. Each schedule targets a Session and tracks admission, retry and its next due time. |
|
|
75
|
+
| Skills | Host-supplied, read-only skill discovery. Metadata enters the prompt; `skill.load` reads a skill body or supporting file; `/skills` lists them. |
|
|
76
|
+
| Home | Agent-scope choice of an attached Computer. Each run captures its scoped file tools and current attachment choice. |
|
|
77
|
+
| Subagents | A read-only footer view: one row per Agent this Session sent work to or created, with its live status (working, waiting on you, idle, failed, unavailable). A row opens that Agent's conversation, and `/agents` collapses the rows to one summary row and expands them again. It owns no relationship and no durable state. |
|
|
78
78
|
|
|
79
79
|
An active Goal is quiet system context on every run. Continuation instructions
|
|
80
80
|
apply only to runs admitted by Goal with its continuation correlation; human
|
|
@@ -90,14 +90,21 @@ metadata used for compact notes and appear as ordinary message bubbles.
|
|
|
90
90
|
|
|
91
91
|
Goal pause stops continuation, not already accepted work. Model-requested
|
|
92
92
|
completion or blocking is provisional until successful settlement; a blocked
|
|
93
|
-
goal pauses and resumes like any paused goal.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
goal pauses and resumes like any paused goal. Each run's goal tools are fenced
|
|
94
|
+
by the goal revision that run saw, so a later follow-up command (for example a
|
|
95
|
+
parent's message to a sub-agent) can complete, block, clear or resume a goal
|
|
96
|
+
that paused on its own; a goal a person paused or stopped accepts only
|
|
97
|
+
completion from the model, and only a person clears a live goal. When a
|
|
98
|
+
tool-less goal turn ends while a recorded sub-agent command is live, the goal
|
|
99
|
+
raises `goal-waiting` attention and rechecks on a timer instead of counting a
|
|
100
|
+
stall; it continues once the child settles or replies. Only a live or paused
|
|
101
|
+
goal asks before a replacement, which uses the observed revision to avoid
|
|
102
|
+
replacing a newer goal; the replaced goal is recorded as cleared. When a runtime
|
|
103
|
+
restart (a Hub deploy or eviction, a Local service restart) interrupts a goal
|
|
104
|
+
turn, the turn resumes and the goal stays live. A live goal pauses when any turn
|
|
105
|
+
of its Session fails, cannot resume or is stopped by a person (Esc), including
|
|
106
|
+
the person's own messages ("The last turn failed"). A missing extension origin
|
|
107
|
+
does not establish a human sender; mail and host admissions can also lack one.
|
|
101
108
|
|
|
102
109
|
Goal's shared wire and durable row use `pauseCause: { kind, detail }` while
|
|
103
110
|
paused and `null` while active. The closed kinds are `person`, `blocked`,
|
package/dist/goal/contract.d.ts
CHANGED
|
@@ -44,9 +44,12 @@ export declare const EndedGoal: Schema.Struct<{
|
|
|
44
44
|
readonly sequence: Schema.Int;
|
|
45
45
|
/** Objective that finished or was cleared. */
|
|
46
46
|
readonly objective: Schema.String;
|
|
47
|
-
/** Whether the model achieved the goal or
|
|
47
|
+
/** Whether the model achieved the goal or it was cleared. */
|
|
48
48
|
readonly outcome: Schema.Literals<readonly ["achieved", "cleared"]>;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The model's evidence for an achieved goal, or its explanation for
|
|
51
|
+
* clearing its own paused goal; null when a person cleared it.
|
|
52
|
+
*/
|
|
50
53
|
readonly reason: Schema.NullOr<Schema.String>;
|
|
51
54
|
/** Completion time in epoch milliseconds. */
|
|
52
55
|
readonly endedAt: Schema.Int;
|
|
@@ -74,7 +77,9 @@ export declare const GoalCommandInput: Schema.Union<readonly [Schema.Struct<{
|
|
|
74
77
|
readonly confirmed: Schema.optional<Schema.Boolean>;
|
|
75
78
|
}>, Schema.Struct<{
|
|
76
79
|
/** Select an existing-goal control. */
|
|
77
|
-
readonly action: Schema.Literals<readonly ["pause", "resume", "replace", "clear"]>;
|
|
80
|
+
readonly action: Schema.Literals<readonly ["pause", "resume", "replace", "clear", "clear-ask"]>;
|
|
81
|
+
/** The human's answer to "clear this goal?"; absent for a typed clear. */
|
|
82
|
+
readonly confirmed: Schema.optional<Schema.Boolean>;
|
|
78
83
|
}>, Schema.Struct<{}>]>;
|
|
79
84
|
/** Goal's contract: Session-scope human controls and one projection slot. */
|
|
80
85
|
export declare const GoalProjection: Schema.Struct<{
|
|
@@ -102,9 +107,12 @@ export declare const GoalProjection: Schema.Struct<{
|
|
|
102
107
|
readonly sequence: Schema.Int;
|
|
103
108
|
/** Objective that finished or was cleared. */
|
|
104
109
|
readonly objective: Schema.String;
|
|
105
|
-
/** Whether the model achieved the goal or
|
|
110
|
+
/** Whether the model achieved the goal or it was cleared. */
|
|
106
111
|
readonly outcome: Schema.Literals<readonly ["achieved", "cleared"]>;
|
|
107
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The model's evidence for an achieved goal, or its explanation for
|
|
114
|
+
* clearing its own paused goal; null when a person cleared it.
|
|
115
|
+
*/
|
|
108
116
|
readonly reason: Schema.NullOr<Schema.String>;
|
|
109
117
|
/** Completion time in epoch milliseconds. */
|
|
110
118
|
readonly endedAt: Schema.Int;
|
|
@@ -138,7 +146,9 @@ export declare const goalContract: {
|
|
|
138
146
|
readonly confirmed: Schema.optional<Schema.Boolean>;
|
|
139
147
|
}>, Schema.Struct<{
|
|
140
148
|
/** Select an existing-goal control. */
|
|
141
|
-
readonly action: Schema.Literals<readonly ["pause", "resume", "replace", "clear"]>;
|
|
149
|
+
readonly action: Schema.Literals<readonly ["pause", "resume", "replace", "clear", "clear-ask"]>;
|
|
150
|
+
/** The human's answer to "clear this goal?"; absent for a typed clear. */
|
|
151
|
+
readonly confirmed: Schema.optional<Schema.Boolean>;
|
|
142
152
|
}>, Schema.Struct<{}>]>;
|
|
143
153
|
/** Return the server-owned conversation step. */
|
|
144
154
|
readonly output: Schema.declare<import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1, import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1>;
|
|
@@ -273,9 +283,12 @@ export declare const goalContract: {
|
|
|
273
283
|
readonly sequence: Schema.Int;
|
|
274
284
|
/** Objective that finished or was cleared. */
|
|
275
285
|
readonly objective: Schema.String;
|
|
276
|
-
/** Whether the model achieved the goal or
|
|
286
|
+
/** Whether the model achieved the goal or it was cleared. */
|
|
277
287
|
readonly outcome: Schema.Literals<readonly ["achieved", "cleared"]>;
|
|
278
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* The model's evidence for an achieved goal, or its explanation for
|
|
290
|
+
* clearing its own paused goal; null when a person cleared it.
|
|
291
|
+
*/
|
|
279
292
|
readonly reason: Schema.NullOr<Schema.String>;
|
|
280
293
|
/** Completion time in epoch milliseconds. */
|
|
281
294
|
readonly endedAt: Schema.Int;
|
package/dist/goal/contract.js
CHANGED
|
@@ -46,9 +46,12 @@ export const EndedGoal = Schema.Struct({
|
|
|
46
46
|
sequence: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
47
47
|
/** Objective that finished or was cleared. */
|
|
48
48
|
objective: Objective,
|
|
49
|
-
/** Whether the model achieved the goal or
|
|
49
|
+
/** Whether the model achieved the goal or it was cleared. */
|
|
50
50
|
outcome: Schema.Literals(["achieved", "cleared"]),
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* The model's evidence for an achieved goal, or its explanation for
|
|
53
|
+
* clearing its own paused goal; null when a person cleared it.
|
|
54
|
+
*/
|
|
52
55
|
reason: Schema.NullOr(GoalReason),
|
|
53
56
|
/** Completion time in epoch milliseconds. */
|
|
54
57
|
endedAt: EpochMillis,
|
|
@@ -76,7 +79,15 @@ export const GoalCommandInput = Schema.Union([
|
|
|
76
79
|
}),
|
|
77
80
|
Schema.Struct({
|
|
78
81
|
/** Select an existing-goal control. */
|
|
79
|
-
action: Schema.Literals([
|
|
82
|
+
action: Schema.Literals([
|
|
83
|
+
"pause",
|
|
84
|
+
"resume",
|
|
85
|
+
"replace",
|
|
86
|
+
"clear",
|
|
87
|
+
"clear-ask",
|
|
88
|
+
]),
|
|
89
|
+
/** The human's answer to "clear this goal?"; absent for a typed clear. */
|
|
90
|
+
confirmed: Schema.optional(Schema.Boolean),
|
|
80
91
|
}),
|
|
81
92
|
Schema.Struct({}),
|
|
82
93
|
]);
|
package/dist/goal/host.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type RosteredExtensionV0 } from "@fungi.computer/shiitake/extensions";
|
|
2
|
+
import type { SubagentSource } from "../subagents/host.js";
|
|
2
3
|
/** The commands this extension advertises to every client. */
|
|
3
4
|
export declare const goalCatalog: import("@fungi.computer/shiitake/extensions").Json;
|
|
4
|
-
/** Botanical's Session goal
|
|
5
|
+
/** Botanical's Session goal; hosts with created-Agent facts include them in progress. */
|
|
6
|
+
export declare function goalExtensionFor(subagents?: SubagentSource): RosteredExtensionV0;
|
|
7
|
+
/** Ordinary roster when no created-Agent directory is available. */
|
|
5
8
|
export declare const goalExtension: RosteredExtensionV0;
|
package/dist/goal/host.js
CHANGED
|
@@ -9,16 +9,19 @@ import * as Schema from "effect/Schema";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { ask, catalog, declined, done, freshRequestId, } from "../internal/catalog.js";
|
|
11
11
|
import { EndedGoal, endedGoalLimit, Goal, GoalCommandInput, goalContract, GoalReason, Objective, } from "./contract.js";
|
|
12
|
+
import { clearConfirmation, goalMenuPrompt, replaceConfirmation, } from "./prompts.js";
|
|
12
13
|
/** The rules the model follows; the describe page and an active goal's prompt share them. */
|
|
13
|
-
const rules = "You may start a goal yourself when none is current. Before marking complete audit every requirement against evidence; complete only after actual achievement. Block only after the same external impasse for three consecutive goal turns; a blocked goal
|
|
14
|
+
const rules = "You may start a goal yourself when none is current. Before marking complete audit every requirement against evidence; complete only after actual achievement. Block only after the same external impasse for three consecutive goal turns; a blocked goal stays paused until it is resumed. You may complete, block, clear or resume a goal that paused on its own (stalled, blocked or failed work); a goal a person paused or stopped you may only complete, and only a person clears a live goal. Human pause/resume/clear remain human controls. Use plan.update_plan for multi-step work when available.";
|
|
14
15
|
/** Hashed prose and policy: every constant goal reads comes from here. */
|
|
15
16
|
const config = {
|
|
16
17
|
continuation: "Continue working toward the current goal described in the goal section. Find goal.get_goal and goal.update_goal with forage.search({query:'goal'}) and forage.describe. Audit every requirement against evidence before goal.update_goal({status:'complete', reason}). If the same external blocker persists for three goal turns, report blocked with a reason. Check for progress rather than repeating yourself.",
|
|
17
|
-
moduleDescription: "Session goal tools: create_goal({objective}), get_goal({}), update_goal({status:'complete'|'blocked', reason}). " +
|
|
18
|
+
moduleDescription: "Session goal tools: create_goal({objective}), get_goal({}), update_goal({status:'complete'|'blocked'|'clear'|'resume', reason}). " +
|
|
18
19
|
rules,
|
|
19
20
|
createDescription: "Create a Session goal, including autonomously. Fails while a goal is active or paused; a finished goal is history.",
|
|
20
21
|
getDescription: "Read the current goal. A requested terminal state remains provisional until successful settlement.",
|
|
21
|
-
updateDescription: "Request complete only after actual achievement, or blocked only after the same external impasse for three consecutive goal turns; settlement commits the request. Complete ends the goal; blocked pauses it for a person.",
|
|
22
|
+
updateDescription: "Request complete only after actual achievement, or blocked only after the same external impasse for three consecutive goal turns; both also work on a paused goal, and settlement commits the request. Complete ends the goal; blocked pauses it for a person. clear abandons a paused goal with your reason. resume reactivates a paused goal now. A goal a person paused or stopped accepts only complete.",
|
|
23
|
+
waitCheckMs: 60_000,
|
|
24
|
+
waitingSummary: "Waiting on sub-agent",
|
|
22
25
|
promptLimit: 1_024,
|
|
23
26
|
stallTurns: 3,
|
|
24
27
|
stallReason: "Stalled: no progress for 3 turns",
|
|
@@ -54,7 +57,7 @@ const GoalRow = Schema.Struct({
|
|
|
54
57
|
sessionId: Schema.String,
|
|
55
58
|
/** A model-requested terminal, committed only by its run's success. */
|
|
56
59
|
pending: Schema.NullOr(Schema.Struct({
|
|
57
|
-
state: Schema.Literals(["complete", "blocked"]),
|
|
60
|
+
state: Schema.Literals(["complete", "blocked", "cleared"]),
|
|
58
61
|
reason: GoalReason,
|
|
59
62
|
})),
|
|
60
63
|
stalledTurns: Schema.Int,
|
|
@@ -90,6 +93,7 @@ function committed(row) {
|
|
|
90
93
|
function commitGoal(ctx, row) {
|
|
91
94
|
return Effect.gen(function* () {
|
|
92
95
|
yield* ctx.state.goals.put(row);
|
|
96
|
+
yield* ctx.attention.clear(row.sessionId, "waiting-goal");
|
|
93
97
|
if (row.state === "paused" &&
|
|
94
98
|
(row.pauseCause?.kind === "blocked" || row.pauseCause?.kind === "stalled"))
|
|
95
99
|
yield* ctx.attention
|
|
@@ -150,6 +154,7 @@ function finishGoal(ctx, goal, outcome, reason) {
|
|
|
150
154
|
});
|
|
151
155
|
yield* ctx.state.goals.remove(goal.sessionId);
|
|
152
156
|
yield* ctx.attention.clear(goal.sessionId, "paused-goal");
|
|
157
|
+
yield* ctx.attention.clear(goal.sessionId, "waiting-goal");
|
|
153
158
|
});
|
|
154
159
|
}
|
|
155
160
|
/** Pause or reactivate the current goal without touching admitted work. */
|
|
@@ -197,7 +202,7 @@ function continueIfIdle(ctx, goal, requestId) {
|
|
|
197
202
|
* settles here like any other, so the goal keeps going. An achieved goal
|
|
198
203
|
* finishes and leaves the current slot.
|
|
199
204
|
*/
|
|
200
|
-
function settle(ctx, run) {
|
|
205
|
+
function settle(ctx, run, subagents) {
|
|
201
206
|
return Effect.gen(function* () {
|
|
202
207
|
const found = yield* ctx.state.goals.get(run.sessionId);
|
|
203
208
|
if (Option.isNone(found))
|
|
@@ -220,6 +225,9 @@ function settle(ctx, run) {
|
|
|
220
225
|
yield* commitGoal(ctx, paused(goal, "no-terminal", ctx.config.noTerminalReason));
|
|
221
226
|
return yield* ctx.observe(run.sessionId);
|
|
222
227
|
}
|
|
228
|
+
const waitingForChild = run.toolCalls === 0 &&
|
|
229
|
+
subagents?.hasLiveChildCommand(run.sessionId) === true &&
|
|
230
|
+
run.origin?.extensionId === goalContract.id;
|
|
223
231
|
const tracked = {
|
|
224
232
|
...goal,
|
|
225
233
|
trackedWorkId: run.workId,
|
|
@@ -228,16 +236,34 @@ function settle(ctx, run) {
|
|
|
228
236
|
? run.toolCalls === 0
|
|
229
237
|
? goal.stalledTurns + 1
|
|
230
238
|
: 0
|
|
231
|
-
:
|
|
239
|
+
: 0,
|
|
232
240
|
};
|
|
233
241
|
const pending = goal.trackedWorkId === run.workId ? goal.pending : null;
|
|
234
242
|
if (pending?.state === "complete")
|
|
235
243
|
yield* finishGoal(ctx, tracked, "achieved", pending.reason);
|
|
244
|
+
else if (pending?.state === "cleared")
|
|
245
|
+
yield* finishGoal(ctx, tracked, "cleared", pending.reason);
|
|
236
246
|
else if (pending?.state === "blocked")
|
|
237
247
|
yield* commitGoal(ctx, paused(tracked, "blocked", pending.reason));
|
|
238
248
|
else if (goal.state === "active" &&
|
|
249
|
+
!waitingForChild &&
|
|
239
250
|
tracked.stalledTurns >= ctx.config.stallTurns)
|
|
240
251
|
yield* commitGoal(ctx, paused(tracked, "stalled", ctx.config.stallReason));
|
|
252
|
+
else if (goal.state === "active" && waitingForChild) {
|
|
253
|
+
// A tool-free goal turn while a child command is live is waiting, not
|
|
254
|
+
// stalling: show it, and recheck on a timer so the goal resumes when the
|
|
255
|
+
// child settles even if no reply mail wakes this Session.
|
|
256
|
+
yield* commitGoal(ctx, tracked);
|
|
257
|
+
const now = yield* Clock.currentTimeMillis;
|
|
258
|
+
yield* ctx.attention.set(run.sessionId, {
|
|
259
|
+
id: "waiting-goal",
|
|
260
|
+
kind: "goal-waiting",
|
|
261
|
+
inbox: false,
|
|
262
|
+
summary: `${ctx.config.waitingSummary}: ${tracked.objective}`.slice(0, 200),
|
|
263
|
+
since: now,
|
|
264
|
+
});
|
|
265
|
+
yield* ctx.timers.set(waitTimerKey(run.sessionId), now + ctx.config.waitCheckMs);
|
|
266
|
+
}
|
|
241
267
|
else if (goal.state === "active")
|
|
242
268
|
yield* commitGoal(ctx, yield* continueIfIdle(ctx, tracked, `goal-continue:${run.workId}`));
|
|
243
269
|
else
|
|
@@ -245,6 +271,29 @@ function settle(ctx, run) {
|
|
|
245
271
|
yield* ctx.observe(run.sessionId);
|
|
246
272
|
});
|
|
247
273
|
}
|
|
274
|
+
const waitTimerPrefix = "waiting:";
|
|
275
|
+
function waitTimerKey(sessionId) {
|
|
276
|
+
return `${waitTimerPrefix}${sessionId}`;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Recheck a goal that is waiting on a child command: keep waiting while the
|
|
280
|
+
* child is live, otherwise admit the next goal turn (an idle-only, replayed
|
|
281
|
+
* admission, so a Session that already moved on is unaffected).
|
|
282
|
+
*/
|
|
283
|
+
function recheckWaiting(ctx, timer, subagents) {
|
|
284
|
+
return Effect.gen(function* () {
|
|
285
|
+
if (!timer.key.startsWith(waitTimerPrefix))
|
|
286
|
+
return;
|
|
287
|
+
const sessionId = timer.key.slice(waitTimerPrefix.length);
|
|
288
|
+
const found = yield* ctx.state.goals.get(sessionId);
|
|
289
|
+
if (Option.isNone(found) || found.value.state !== "active")
|
|
290
|
+
return;
|
|
291
|
+
if (subagents?.hasLiveChildCommand(sessionId) === true)
|
|
292
|
+
return yield* ctx.timers.set(timer.key, timer.firedAt + ctx.config.waitCheckMs);
|
|
293
|
+
yield* commitGoal(ctx, yield* continueIfIdle(ctx, found.value, `goal-wait:${found.value.trackedWorkId}`));
|
|
294
|
+
yield* ctx.observe(sessionId);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
248
297
|
/** Run one tool transaction and preserve its owner-admitted diagnostic. */
|
|
249
298
|
function toolCall(ctx, operation, body) {
|
|
250
299
|
return Effect.runPromiseExit(ctx.transact(body)).then((exit) => {
|
|
@@ -267,9 +316,24 @@ function toolCall(ctx, operation, body) {
|
|
|
267
316
|
}
|
|
268
317
|
// Mycelium's maintained operation interface takes Zod for model-tool I/O.
|
|
269
318
|
const toolObjective = z.string().min(1).max(4_000);
|
|
270
|
-
const toolState = z.enum([
|
|
271
|
-
|
|
272
|
-
|
|
319
|
+
const toolState = z.enum([
|
|
320
|
+
"active",
|
|
321
|
+
"paused",
|
|
322
|
+
"complete",
|
|
323
|
+
"blocked",
|
|
324
|
+
"cleared",
|
|
325
|
+
]);
|
|
326
|
+
/** The model-facing goal tools for one run, fenced by the goal it saw. */
|
|
327
|
+
function goalModule(ctx, sessionId, workId, initialRevision) {
|
|
328
|
+
const currentGoal = Effect.gen(function* () {
|
|
329
|
+
const found = yield* ctx.state.goals.get(sessionId);
|
|
330
|
+
if (Option.isNone(found))
|
|
331
|
+
return yield* Effect.fail(conflict("No Session goal"));
|
|
332
|
+
if (found.value.revision !== initialRevision &&
|
|
333
|
+
found.value.revision !== `run:${workId}`)
|
|
334
|
+
return yield* Effect.fail(conflict("Session goal work guard is stale"));
|
|
335
|
+
return found.value;
|
|
336
|
+
});
|
|
273
337
|
return Mycelium.module({
|
|
274
338
|
id: "botanical.session-goal",
|
|
275
339
|
name: "goal",
|
|
@@ -329,35 +393,68 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
329
393
|
replay: "unsafe",
|
|
330
394
|
description: ctx.config.updateDescription,
|
|
331
395
|
input: z.strictObject({
|
|
332
|
-
status: z.enum(["complete", "blocked"]),
|
|
396
|
+
status: z.enum(["complete", "blocked", "clear", "resume"]),
|
|
333
397
|
reason: z.string().min(1).max(1_000),
|
|
334
398
|
}),
|
|
335
399
|
output: z.strictObject({
|
|
336
400
|
objective: toolObjective,
|
|
337
|
-
state: z.enum(["complete", "blocked"]),
|
|
401
|
+
state: z.enum(["active", "complete", "blocked", "cleared"]),
|
|
338
402
|
reason: z.string(),
|
|
339
403
|
}),
|
|
340
404
|
execute: ({ status, reason }) => toolCall(ctx, "update_goal", Effect.gen(function* () {
|
|
341
405
|
const decoded = yield* Schema.decodeUnknownEffect(GoalReason)(reason).pipe(Effect.mapError(() => conflict("Goal reason is invalid")));
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
406
|
+
const goal = yield* currentGoal;
|
|
407
|
+
// A person's pause or stop is theirs: the model may only finish it.
|
|
408
|
+
if (status !== "complete" && heldByPerson(goal))
|
|
409
|
+
return yield* Effect.fail(conflict("A person paused or stopped this goal; only complete is available to you"));
|
|
410
|
+
if (status === "resume") {
|
|
411
|
+
const resumed = yield* modelResume(goal, workId);
|
|
412
|
+
yield* commitGoal(ctx, resumed);
|
|
413
|
+
yield* ctx.observe(sessionId);
|
|
414
|
+
return {
|
|
415
|
+
objective: goal.objective,
|
|
416
|
+
state: "active",
|
|
417
|
+
reason: decoded,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
// Only a person discards a live goal; the model may clear a paused one.
|
|
421
|
+
if (status === "clear" && goal.state !== "paused")
|
|
422
|
+
return yield* Effect.fail(conflict("Only a paused Session goal can be cleared"));
|
|
423
|
+
const state = status === "clear" ? "cleared" : status;
|
|
347
424
|
yield* commitGoal(ctx, {
|
|
348
|
-
...
|
|
349
|
-
|
|
425
|
+
...goal,
|
|
426
|
+
// This run's settlement commits the provisional terminal.
|
|
427
|
+
trackedWorkId: workId,
|
|
428
|
+
pending: { state, reason: decoded },
|
|
350
429
|
});
|
|
351
|
-
return {
|
|
352
|
-
objective: found.value.objective,
|
|
353
|
-
state: status,
|
|
354
|
-
reason: decoded,
|
|
355
|
-
};
|
|
430
|
+
return { objective: goal.objective, state, reason: decoded };
|
|
356
431
|
})),
|
|
357
432
|
}),
|
|
358
433
|
},
|
|
359
434
|
});
|
|
360
435
|
}
|
|
436
|
+
/** A person's pause or Esc; only a person resumes, blocks or clears it. */
|
|
437
|
+
function heldByPerson(goal) {
|
|
438
|
+
return (goal.state === "paused" &&
|
|
439
|
+
(goal.pauseCause?.kind === "person" ||
|
|
440
|
+
goal.pauseCause?.kind === "cancelled"));
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* The model resumes its own paused goal. The resuming run becomes the
|
|
444
|
+
* tracked work, so its settlement admits the next goal turn.
|
|
445
|
+
*/
|
|
446
|
+
function modelResume(goal, workId) {
|
|
447
|
+
if (goal.state !== "paused")
|
|
448
|
+
return Effect.fail(conflict("Session goal is not paused"));
|
|
449
|
+
return Effect.succeed({
|
|
450
|
+
...goal,
|
|
451
|
+
state: "active",
|
|
452
|
+
pauseCause: null,
|
|
453
|
+
pending: null,
|
|
454
|
+
stalledTurns: 0,
|
|
455
|
+
trackedWorkId: workId,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
361
458
|
function pauseGoal(ctx, sessionId) {
|
|
362
459
|
return changeActivity(ctx, sessionId, "paused").pipe(Effect.map(committed));
|
|
363
460
|
}
|
|
@@ -420,6 +517,7 @@ function setGoal(ctx, sessionId, input) {
|
|
|
420
517
|
return { goal: committed(row), accepted };
|
|
421
518
|
});
|
|
422
519
|
}
|
|
520
|
+
/** The one-line state the model prompt and attention summaries carry. */
|
|
423
521
|
function goalLine(goal) {
|
|
424
522
|
if (goal.state === "active")
|
|
425
523
|
return `Pursuing goal: ${goal.objective}`;
|
|
@@ -453,21 +551,15 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
453
551
|
if (!("action" in input)) {
|
|
454
552
|
if (current === undefined)
|
|
455
553
|
return ask(objectivePrompt, { action: "set", requestId: yield* freshRequestId }, "objective");
|
|
456
|
-
|
|
457
|
-
return ask({
|
|
458
|
-
type: "choice",
|
|
459
|
-
message: goalLine(goal),
|
|
460
|
-
options: [
|
|
461
|
-
goal.state === "active"
|
|
462
|
-
? { value: "pause", label: "Pause goal" }
|
|
463
|
-
: { value: "resume", label: "Resume goal" },
|
|
464
|
-
{ value: "clear", label: "Clear goal" },
|
|
465
|
-
{ value: "replace", label: "Replace goal" },
|
|
466
|
-
],
|
|
467
|
-
}, {}, "action");
|
|
554
|
+
return ask(goalMenuPrompt(committed(current), yield* Clock.currentTimeMillis), {}, "action");
|
|
468
555
|
}
|
|
469
556
|
switch (input.action) {
|
|
557
|
+
case "clear-ask":
|
|
558
|
+
// The menu's Clear: Keep is the focused answer to this destructive step.
|
|
559
|
+
return ask(clearConfirmation, { action: "clear" }, "confirmed");
|
|
470
560
|
case "clear":
|
|
561
|
+
if (input.confirmed === false)
|
|
562
|
+
return declined({ type: "info", message: "Kept the goal." });
|
|
471
563
|
// The chat's ending line confirms a clear; only an empty one says so.
|
|
472
564
|
return (yield* clearGoal(ctx, sessionId))
|
|
473
565
|
? { type: "done" }
|
|
@@ -483,7 +575,6 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
483
575
|
action: "set",
|
|
484
576
|
requestId: yield* freshRequestId,
|
|
485
577
|
expectedRevision: current?.revision ?? null,
|
|
486
|
-
confirmed: true,
|
|
487
578
|
}, "objective");
|
|
488
579
|
case "set": {
|
|
489
580
|
const retried = current?.revisionRequestId === input.requestId;
|
|
@@ -492,12 +583,7 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
492
583
|
if (replacing && input.confirmed === false)
|
|
493
584
|
return declined({ type: "info", message: "Kept the current goal." });
|
|
494
585
|
if (replacing && input.confirmed !== true)
|
|
495
|
-
return ask({
|
|
496
|
-
type: "confirmation",
|
|
497
|
-
message: `Replace the ${current.state === "active" ? "live" : "paused"} goal?\nCurrent: ${current.objective}\nNew: ${input.objective}`,
|
|
498
|
-
confirmLabel: "Replace and start",
|
|
499
|
-
cancelLabel: "Keep current goal",
|
|
500
|
-
}, {
|
|
586
|
+
return ask(replaceConfirmation(committed(current), input.objective), {
|
|
501
587
|
action: "set",
|
|
502
588
|
objective: input.objective,
|
|
503
589
|
requestId: input.requestId,
|
|
@@ -556,69 +642,80 @@ export const goalCatalog = catalog([
|
|
|
556
642
|
},
|
|
557
643
|
},
|
|
558
644
|
]);
|
|
559
|
-
/** Botanical's Session goal
|
|
560
|
-
export
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
"
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
645
|
+
/** Botanical's Session goal; hosts with created-Agent facts include them in progress. */
|
|
646
|
+
export function goalExtensionFor(subagents) {
|
|
647
|
+
return (defineExtension({
|
|
648
|
+
id: goalContract.id,
|
|
649
|
+
version: "7.0.0",
|
|
650
|
+
})
|
|
651
|
+
.config(config)
|
|
652
|
+
// A command anchor cannot be converted into a historical Session position.
|
|
653
|
+
// Old preview state must be republished separately, never guessed here.
|
|
654
|
+
.state({ version: 7, migrations: {}, collections })
|
|
655
|
+
.define({
|
|
656
|
+
attention: true,
|
|
657
|
+
prompt: (ctx, { sessionId, origin }) => Effect.gen(function* () {
|
|
658
|
+
const goal = Option.getOrNull(yield* ctx.state.goals.get(sessionId));
|
|
659
|
+
const text = goal === null
|
|
660
|
+
? ctx.config.idle
|
|
661
|
+
: (goal.state === "active"
|
|
662
|
+
? ctx.config.guide
|
|
663
|
+
: ctx.config.idle) +
|
|
664
|
+
"\n" +
|
|
665
|
+
goalLine(committed(goal));
|
|
666
|
+
return [
|
|
667
|
+
{
|
|
668
|
+
name: "goal",
|
|
669
|
+
text: boundedPrompt(text, ctx.config.promptLimit),
|
|
670
|
+
},
|
|
671
|
+
...(goal?.state === "active" &&
|
|
672
|
+
origin?.extensionId === goalContract.id &&
|
|
673
|
+
origin.correlation !== null
|
|
674
|
+
? [
|
|
675
|
+
{
|
|
676
|
+
name: "goal-continuation",
|
|
677
|
+
text: boundedPrompt(ctx.config.continuation, ctx.config.promptLimit),
|
|
678
|
+
},
|
|
679
|
+
]
|
|
680
|
+
: []),
|
|
681
|
+
];
|
|
682
|
+
}),
|
|
683
|
+
catalog: goalCatalog,
|
|
684
|
+
projection: {
|
|
685
|
+
schema: goalContract.projection.schema,
|
|
686
|
+
read: (ctx, sessionId) => {
|
|
687
|
+
const goal = Option.getOrUndefined(ctx.state.goals.peek(sessionId));
|
|
688
|
+
const endings = Option.getOrUndefined(ctx.state.endings.peek(sessionId));
|
|
689
|
+
if (goal === undefined && endings === undefined)
|
|
690
|
+
return Option.none();
|
|
691
|
+
const ended = endings?.ended ?? [];
|
|
692
|
+
return Option.some({
|
|
693
|
+
goal: goal === undefined ? null : committed(goal),
|
|
694
|
+
ended,
|
|
695
|
+
omitted: (endings?.total ?? 0) - ended.length,
|
|
696
|
+
});
|
|
581
697
|
},
|
|
582
|
-
...(goal?.state === "active" &&
|
|
583
|
-
origin?.extensionId === goalContract.id &&
|
|
584
|
-
origin.correlation !== null
|
|
585
|
-
? [
|
|
586
|
-
{
|
|
587
|
-
name: "goal-continuation",
|
|
588
|
-
text: boundedPrompt(ctx.config.continuation, ctx.config.promptLimit),
|
|
589
|
-
},
|
|
590
|
-
]
|
|
591
|
-
: []),
|
|
592
|
-
];
|
|
593
|
-
}),
|
|
594
|
-
catalog: goalCatalog,
|
|
595
|
-
projection: {
|
|
596
|
-
schema: goalContract.projection.schema,
|
|
597
|
-
read: (ctx, sessionId) => {
|
|
598
|
-
const goal = Option.getOrUndefined(ctx.state.goals.peek(sessionId));
|
|
599
|
-
const endings = Option.getOrUndefined(ctx.state.endings.peek(sessionId));
|
|
600
|
-
if (goal === undefined && endings === undefined)
|
|
601
|
-
return Option.none();
|
|
602
|
-
const ended = endings?.ended ?? [];
|
|
603
|
-
return Option.some({
|
|
604
|
-
goal: goal === undefined ? null : committed(goal),
|
|
605
|
-
ended,
|
|
606
|
-
omitted: (endings?.total ?? 0) - ended.length,
|
|
607
|
-
});
|
|
608
698
|
},
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
})
|
|
699
|
+
hooks: {
|
|
700
|
+
onSettled: (ctx, page) => Effect.forEach(page, (run) => settle(ctx, run, subagents), {
|
|
701
|
+
discard: true,
|
|
702
|
+
}),
|
|
703
|
+
onTimer: (ctx, timer) => recheckWaiting(ctx, timer, subagents),
|
|
704
|
+
},
|
|
705
|
+
tools: (ctx, { sessionId, workId }) => ctx.state.goals
|
|
706
|
+
.get(sessionId)
|
|
707
|
+
.pipe(Effect.map((found) => Option.some(goalModule(ctx, sessionId, workId, Option.isSome(found) ? found.value.revision : null)))),
|
|
708
|
+
commands: {
|
|
709
|
+
specs: goalContract.commands,
|
|
710
|
+
handlers: (ctx) => ({
|
|
711
|
+
goal: ({ sessionId, input }) => humanGoal(ctx, sessionId, input),
|
|
712
|
+
pause: ({ sessionId }) => pauseGoal(ctx, sessionId),
|
|
713
|
+
resume: ({ sessionId }) => resumeGoal(ctx, sessionId),
|
|
714
|
+
clear: ({ sessionId }) => clearGoal(ctx, sessionId).pipe(Effect.as({})),
|
|
715
|
+
set: ({ sessionId, input }) => setGoal(ctx, sessionId, input),
|
|
716
|
+
}),
|
|
717
|
+
},
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
/** Ordinary roster when no created-Agent directory is available. */
|
|
721
|
+
export const goalExtension = goalExtensionFor();
|