@fungi.computer/shiitake-extensions 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -21
- package/dist/goal/contract.d.ts +27 -8
- package/dist/goal/contract.js +20 -3
- package/dist/goal/host.d.ts +4 -1
- package/dist/goal/host.js +375 -128
- package/dist/goal/prompts.d.ts +73 -0
- package/dist/goal/prompts.js +98 -0
- package/dist/home/host.js +9 -3
- package/dist/internal/headline.d.ts +7 -0
- package/dist/internal/headline.js +18 -0
- package/dist/loop/client.js +1 -1
- package/dist/loop/contract.d.ts +6 -2
- package/dist/loop/contract.js +10 -1
- package/dist/loop/host.js +10 -12
- package/dist/loop/prompts.d.ts +56 -0
- package/dist/loop/prompts.js +54 -0
- package/dist/question/answer-flow.d.ts +27 -0
- package/dist/question/answer-flow.js +115 -0
- package/dist/question/client.d.ts +1 -1
- package/dist/question/client.js +22 -3
- package/dist/question/contract.d.ts +202 -60
- package/dist/question/contract.js +78 -22
- package/dist/question/host.d.ts +4 -0
- package/dist/question/host.js +348 -304
- package/dist/roster/conversation-agents.d.ts +22 -15
- package/dist/roster/conversation-agents.js +161 -37
- package/dist/roster.d.ts +8 -2
- package/dist/roster.js +33 -6
- package/dist/skills/host.js +20 -2
- package/dist/subagents/client.js +64 -20
- package/dist/subagents/contract.d.ts +123 -10
- package/dist/subagents/contract.js +38 -5
- package/dist/subagents/host.d.ts +5 -5
- package/dist/subagents/host.js +152 -22
- package/docs/api/goal/contract.md +5 -2
- package/docs/api/goal/host.md +22 -1
- package/docs/api/home/host.md +6 -5
- package/docs/api/loop/contract.md +5 -2
- package/docs/api/question/client.md +2 -1
- package/docs/api/question/contract.md +183 -51
- package/docs/api/question/host.md +30 -0
- package/docs/api/roster/conversation-agents.md +15 -7
- package/docs/api/roster.md +39 -2
- package/docs/api/subagents/contract.md +101 -28
- package/docs/api/subagents/host.md +5 -2
- package/package.json +5 -6
package/dist/goal/host.js
CHANGED
|
@@ -9,16 +9,23 @@ 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
|
-
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}). " +
|
|
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. If there is nothing to do until something outside this turn happens (a child's reply, a person, the train, main), call goal.wait({reason, fallbackMinutes}) and end the turn instead of polling.",
|
|
18
|
+
moduleDescription: "Session goal tools: create_goal({objective}), get_goal({}), update_goal({status:'complete'|'blocked'|'clear'|'resume', reason}), wait({reason, fallbackMinutes}). " +
|
|
18
19
|
rules,
|
|
19
|
-
createDescription: "Create a Session goal, including autonomously. Fails while a goal is active or paused; a finished goal is history.",
|
|
20
|
+
createDescription: "Create a Session goal, including autonomously. Fails while a goal is active or paused; a finished goal is history. After update_goal complete (or clear on a paused goal) in this same run, it queues the replacement, which takes over when this run settles.",
|
|
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
|
+
waitDescription: "Say the goal is waiting on something outside this turn, then end the turn. The goal stays active and admits no goal turn and no stall count until another run in this Session settles (a child's reply, a person's message, a loop tick) or fallbackMinutes (default 20, 1 to 60) pass. Use it instead of polling.",
|
|
25
|
+
waitDefaultMinutes: 20,
|
|
26
|
+
waitMaxMinutes: 60,
|
|
27
|
+
waitingSummary: "Waiting on sub-agent",
|
|
28
|
+
waitingOnSummary: "Waiting",
|
|
22
29
|
promptLimit: 1_024,
|
|
23
30
|
stallTurns: 3,
|
|
24
31
|
stallReason: "Stalled: no progress for 3 turns",
|
|
@@ -54,10 +61,21 @@ const GoalRow = Schema.Struct({
|
|
|
54
61
|
sessionId: Schema.String,
|
|
55
62
|
/** A model-requested terminal, committed only by its run's success. */
|
|
56
63
|
pending: Schema.NullOr(Schema.Struct({
|
|
57
|
-
state: Schema.Literals(["complete", "blocked"]),
|
|
64
|
+
state: Schema.Literals(["complete", "blocked", "cleared"]),
|
|
58
65
|
reason: GoalReason,
|
|
59
66
|
})),
|
|
60
67
|
stalledTurns: Schema.Int,
|
|
68
|
+
/**
|
|
69
|
+
* The goal waits on something outside its turns. `until` is null while the
|
|
70
|
+
* requesting run is still running; its settlement fixes the fallback instant.
|
|
71
|
+
*/
|
|
72
|
+
waiting: Schema.optionalKey(Schema.NullOr(Schema.Struct({
|
|
73
|
+
reason: GoalReason,
|
|
74
|
+
fallbackMs: Schema.Int,
|
|
75
|
+
until: Schema.NullOr(Schema.Int),
|
|
76
|
+
}))),
|
|
77
|
+
/** A replacement objective queued after this run's complete or clear. */
|
|
78
|
+
successor: Schema.optionalKey(Schema.NullOr(Objective)),
|
|
61
79
|
/** Request that set this revision; its retry replays. */
|
|
62
80
|
revisionRequestId: Schema.String,
|
|
63
81
|
/** Latest work whose settlement advances this goal. */
|
|
@@ -90,6 +108,7 @@ function committed(row) {
|
|
|
90
108
|
function commitGoal(ctx, row) {
|
|
91
109
|
return Effect.gen(function* () {
|
|
92
110
|
yield* ctx.state.goals.put(row);
|
|
111
|
+
yield* ctx.attention.clear(row.sessionId, "waiting-goal");
|
|
93
112
|
if (row.state === "paused" &&
|
|
94
113
|
(row.pauseCause?.kind === "blocked" || row.pauseCause?.kind === "stalled"))
|
|
95
114
|
yield* ctx.attention
|
|
@@ -123,6 +142,8 @@ function paused(goal, kind, detail) {
|
|
|
123
142
|
state: "paused",
|
|
124
143
|
pauseCause: { kind, detail },
|
|
125
144
|
pending: null,
|
|
145
|
+
waiting: null,
|
|
146
|
+
successor: null,
|
|
126
147
|
};
|
|
127
148
|
}
|
|
128
149
|
/**
|
|
@@ -150,6 +171,7 @@ function finishGoal(ctx, goal, outcome, reason) {
|
|
|
150
171
|
});
|
|
151
172
|
yield* ctx.state.goals.remove(goal.sessionId);
|
|
152
173
|
yield* ctx.attention.clear(goal.sessionId, "paused-goal");
|
|
174
|
+
yield* ctx.attention.clear(goal.sessionId, "waiting-goal");
|
|
153
175
|
});
|
|
154
176
|
}
|
|
155
177
|
/** Pause or reactivate the current goal without touching admitted work. */
|
|
@@ -162,6 +184,7 @@ function changeActivity(ctx, sessionId, target) {
|
|
|
162
184
|
state: "active",
|
|
163
185
|
pauseCause: null,
|
|
164
186
|
stalledTurns: 0,
|
|
187
|
+
waiting: null,
|
|
165
188
|
}
|
|
166
189
|
: paused(goal, "person", null);
|
|
167
190
|
yield* commitGoal(ctx, changed);
|
|
@@ -197,7 +220,7 @@ function continueIfIdle(ctx, goal, requestId) {
|
|
|
197
220
|
* settles here like any other, so the goal keeps going. An achieved goal
|
|
198
221
|
* finishes and leaves the current slot.
|
|
199
222
|
*/
|
|
200
|
-
function settle(ctx, run) {
|
|
223
|
+
function settle(ctx, run, subagents) {
|
|
201
224
|
return Effect.gen(function* () {
|
|
202
225
|
const found = yield* ctx.state.goals.get(run.sessionId);
|
|
203
226
|
if (Option.isNone(found))
|
|
@@ -220,24 +243,96 @@ function settle(ctx, run) {
|
|
|
220
243
|
yield* commitGoal(ctx, paused(goal, "no-terminal", ctx.config.noTerminalReason));
|
|
221
244
|
return yield* ctx.observe(run.sessionId);
|
|
222
245
|
}
|
|
246
|
+
const goalTurn = run.origin?.extensionId === goalContract.id;
|
|
247
|
+
// A live child command or a question or approval a person has yet to
|
|
248
|
+
// answer is a legitimate wait, whether or not this turn polled.
|
|
249
|
+
const waitingForChild = goalTurn &&
|
|
250
|
+
(subagents?.hasLiveChildCommand(run.sessionId) === true ||
|
|
251
|
+
(yield* ctx.attention.awaitsPerson(run.sessionId)));
|
|
252
|
+
const waiting = goal.waiting ?? null;
|
|
253
|
+
// The run that called goal.wait fixes the fallback instant as it settles.
|
|
254
|
+
const requested = waiting !== null &&
|
|
255
|
+
waiting.until === null &&
|
|
256
|
+
run.workId === goal.trackedWorkId;
|
|
257
|
+
// Any other run that settles while a wait is armed wakes the goal.
|
|
258
|
+
const woken = waiting !== null && waiting.until !== null;
|
|
223
259
|
const tracked = {
|
|
224
260
|
...goal,
|
|
225
261
|
trackedWorkId: run.workId,
|
|
226
|
-
stalledTurns:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
262
|
+
stalledTurns: requested
|
|
263
|
+
? goal.stalledTurns
|
|
264
|
+
: run.origin?.extensionId === goalContract.id &&
|
|
265
|
+
run.origin.correlation === goal.revision
|
|
266
|
+
? run.toolCalls === 0
|
|
267
|
+
? goal.stalledTurns + 1
|
|
268
|
+
: 0
|
|
269
|
+
: 0,
|
|
232
270
|
};
|
|
233
271
|
const pending = goal.trackedWorkId === run.workId ? goal.pending : null;
|
|
234
|
-
if (pending?.state === "complete")
|
|
235
|
-
yield*
|
|
272
|
+
if (pending?.state === "complete" || pending?.state === "cleared") {
|
|
273
|
+
yield* ctx.timers.cancel(waitTimerKey(run.sessionId));
|
|
274
|
+
yield* finishGoal(ctx, tracked, pending.state === "complete" ? "achieved" : "cleared", pending.reason);
|
|
275
|
+
if (goal.successor != null) {
|
|
276
|
+
const next = {
|
|
277
|
+
sessionId: run.sessionId,
|
|
278
|
+
objective: goal.successor,
|
|
279
|
+
state: "active",
|
|
280
|
+
startedAt: yield* Clock.currentTimeMillis,
|
|
281
|
+
pending: null,
|
|
282
|
+
stalledTurns: 0,
|
|
283
|
+
revision: `run:${run.workId}`,
|
|
284
|
+
revisionRequestId: `run:${run.workId}`,
|
|
285
|
+
trackedWorkId: run.workId,
|
|
286
|
+
pauseCause: null,
|
|
287
|
+
waiting: null,
|
|
288
|
+
successor: null,
|
|
289
|
+
};
|
|
290
|
+
yield* commitGoal(ctx, yield* continueIfIdle(ctx, next, `goal-continue:${run.workId}`));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
236
293
|
else if (pending?.state === "blocked")
|
|
237
294
|
yield* commitGoal(ctx, paused(tracked, "blocked", pending.reason));
|
|
295
|
+
else if (goal.state === "active" && requested) {
|
|
296
|
+
// goal.wait: no continuation and no stall; the next settled run or the
|
|
297
|
+
// fallback timer wakes the goal.
|
|
298
|
+
const now = yield* Clock.currentTimeMillis;
|
|
299
|
+
const until = now + waiting.fallbackMs;
|
|
300
|
+
yield* commitGoal(ctx, {
|
|
301
|
+
...tracked,
|
|
302
|
+
waiting: { ...waiting, until },
|
|
303
|
+
});
|
|
304
|
+
yield* ctx.attention.set(run.sessionId, {
|
|
305
|
+
id: "waiting-goal",
|
|
306
|
+
kind: "goal-waiting",
|
|
307
|
+
inbox: false,
|
|
308
|
+
summary: `${ctx.config.waitingOnSummary}: ${waiting.reason}`.slice(0, 200),
|
|
309
|
+
since: now,
|
|
310
|
+
});
|
|
311
|
+
yield* ctx.timers.set(waitTimerKey(run.sessionId), until);
|
|
312
|
+
}
|
|
313
|
+
else if (goal.state === "active" && woken) {
|
|
314
|
+
yield* ctx.timers.cancel(waitTimerKey(run.sessionId));
|
|
315
|
+
yield* commitGoal(ctx, yield* continueIfIdle(ctx, { ...tracked, waiting: null }, `goal-continue:${run.workId}`));
|
|
316
|
+
}
|
|
238
317
|
else if (goal.state === "active" &&
|
|
318
|
+
!waitingForChild &&
|
|
239
319
|
tracked.stalledTurns >= ctx.config.stallTurns)
|
|
240
320
|
yield* commitGoal(ctx, paused(tracked, "stalled", ctx.config.stallReason));
|
|
321
|
+
else if (goal.state === "active" && waitingForChild) {
|
|
322
|
+
// A tool-free goal turn while a child command is live is waiting, not
|
|
323
|
+
// stalling: show it, and recheck on a timer so the goal resumes when the
|
|
324
|
+
// child settles even if no reply mail wakes this Session.
|
|
325
|
+
yield* commitGoal(ctx, tracked);
|
|
326
|
+
const now = yield* Clock.currentTimeMillis;
|
|
327
|
+
yield* ctx.attention.set(run.sessionId, {
|
|
328
|
+
id: "waiting-goal",
|
|
329
|
+
kind: "goal-waiting",
|
|
330
|
+
inbox: false,
|
|
331
|
+
summary: `${ctx.config.waitingSummary}: ${tracked.objective}`.slice(0, 200),
|
|
332
|
+
since: now,
|
|
333
|
+
});
|
|
334
|
+
yield* ctx.timers.set(waitTimerKey(run.sessionId), now + ctx.config.waitCheckMs);
|
|
335
|
+
}
|
|
241
336
|
else if (goal.state === "active")
|
|
242
337
|
yield* commitGoal(ctx, yield* continueIfIdle(ctx, tracked, `goal-continue:${run.workId}`));
|
|
243
338
|
else
|
|
@@ -245,6 +340,37 @@ function settle(ctx, run) {
|
|
|
245
340
|
yield* ctx.observe(run.sessionId);
|
|
246
341
|
});
|
|
247
342
|
}
|
|
343
|
+
const waitTimerPrefix = "waiting:";
|
|
344
|
+
function waitTimerKey(sessionId) {
|
|
345
|
+
return `${waitTimerPrefix}${sessionId}`;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Recheck a goal that is waiting on a child command: keep waiting while the
|
|
349
|
+
* child is live, otherwise admit the next goal turn (an idle-only, replayed
|
|
350
|
+
* admission, so a Session that already moved on is unaffected).
|
|
351
|
+
*/
|
|
352
|
+
function recheckWaiting(ctx, timer, subagents) {
|
|
353
|
+
return Effect.gen(function* () {
|
|
354
|
+
if (!timer.key.startsWith(waitTimerPrefix))
|
|
355
|
+
return;
|
|
356
|
+
const sessionId = timer.key.slice(waitTimerPrefix.length);
|
|
357
|
+
const found = yield* ctx.state.goals.get(sessionId);
|
|
358
|
+
if (Option.isNone(found) || found.value.state !== "active")
|
|
359
|
+
return;
|
|
360
|
+
const armed = found.value.waiting ?? null;
|
|
361
|
+
if (armed !== null) {
|
|
362
|
+
// goal.wait's fallback: a re-armed wait ignores a stale firing.
|
|
363
|
+
if (armed.until === null || armed.until > timer.firedAt)
|
|
364
|
+
return;
|
|
365
|
+
return yield* commitGoal(ctx, yield* continueIfIdle(ctx, { ...found.value, waiting: null, stalledTurns: 0 }, `goal-wait:${found.value.trackedWorkId}`)).pipe(Effect.andThen(ctx.observe(sessionId)));
|
|
366
|
+
}
|
|
367
|
+
if (subagents?.hasLiveChildCommand(sessionId) === true ||
|
|
368
|
+
(yield* ctx.attention.awaitsPerson(sessionId)))
|
|
369
|
+
return yield* ctx.timers.set(timer.key, timer.firedAt + ctx.config.waitCheckMs);
|
|
370
|
+
yield* commitGoal(ctx, yield* continueIfIdle(ctx, found.value, `goal-wait:${found.value.trackedWorkId}`));
|
|
371
|
+
yield* ctx.observe(sessionId);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
248
374
|
/** Run one tool transaction and preserve its owner-admitted diagnostic. */
|
|
249
375
|
function toolCall(ctx, operation, body) {
|
|
250
376
|
return Effect.runPromiseExit(ctx.transact(body)).then((exit) => {
|
|
@@ -267,9 +393,24 @@ function toolCall(ctx, operation, body) {
|
|
|
267
393
|
}
|
|
268
394
|
// Mycelium's maintained operation interface takes Zod for model-tool I/O.
|
|
269
395
|
const toolObjective = z.string().min(1).max(4_000);
|
|
270
|
-
const toolState = z.enum([
|
|
271
|
-
|
|
272
|
-
|
|
396
|
+
const toolState = z.enum([
|
|
397
|
+
"active",
|
|
398
|
+
"paused",
|
|
399
|
+
"complete",
|
|
400
|
+
"blocked",
|
|
401
|
+
"cleared",
|
|
402
|
+
]);
|
|
403
|
+
/** The model-facing goal tools for one run, fenced by the goal it saw. */
|
|
404
|
+
function goalModule(ctx, sessionId, workId, initialRevision) {
|
|
405
|
+
const currentGoal = Effect.gen(function* () {
|
|
406
|
+
const found = yield* ctx.state.goals.get(sessionId);
|
|
407
|
+
if (Option.isNone(found))
|
|
408
|
+
return yield* Effect.fail(conflict("No Session goal"));
|
|
409
|
+
if (found.value.revision !== initialRevision &&
|
|
410
|
+
found.value.revision !== `run:${workId}`)
|
|
411
|
+
return yield* Effect.fail(conflict("Session goal work guard is stale"));
|
|
412
|
+
return found.value;
|
|
413
|
+
});
|
|
273
414
|
return Mycelium.module({
|
|
274
415
|
id: "botanical.session-goal",
|
|
275
416
|
name: "goal",
|
|
@@ -285,8 +426,21 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
285
426
|
}),
|
|
286
427
|
execute: ({ objective }) => toolCall(ctx, "create_goal", Effect.gen(function* () {
|
|
287
428
|
const decoded = yield* Schema.decodeUnknownEffect(Objective)(objective).pipe(Effect.mapError(() => conflict("Goal objective is invalid")));
|
|
288
|
-
|
|
289
|
-
|
|
429
|
+
const existing = yield* ctx.state.goals.get(sessionId);
|
|
430
|
+
if (Option.isSome(existing)) {
|
|
431
|
+
// A goal this run already finished (complete, or clear of a
|
|
432
|
+
// paused one) hands over to the replacement at settlement.
|
|
433
|
+
const retiring = existing.value.trackedWorkId === workId &&
|
|
434
|
+
(existing.value.pending?.state === "complete" ||
|
|
435
|
+
existing.value.pending?.state === "cleared");
|
|
436
|
+
if (!retiring)
|
|
437
|
+
return yield* Effect.fail(conflict("A Session goal is already active or paused; complete it first, or only a person replaces it"));
|
|
438
|
+
yield* ctx.state.goals.put({
|
|
439
|
+
...existing.value,
|
|
440
|
+
successor: decoded,
|
|
441
|
+
});
|
|
442
|
+
return { objective: decoded, state: "active" };
|
|
443
|
+
}
|
|
290
444
|
yield* commitGoal(ctx, {
|
|
291
445
|
sessionId,
|
|
292
446
|
objective: decoded,
|
|
@@ -298,6 +452,8 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
298
452
|
revisionRequestId: `run:${workId}`,
|
|
299
453
|
trackedWorkId: workId,
|
|
300
454
|
pauseCause: null,
|
|
455
|
+
waiting: null,
|
|
456
|
+
successor: null,
|
|
301
457
|
});
|
|
302
458
|
yield* ctx.observe(sessionId);
|
|
303
459
|
return { objective: decoded, state: "active" };
|
|
@@ -325,39 +481,116 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
325
481
|
null,
|
|
326
482
|
}))),
|
|
327
483
|
}),
|
|
484
|
+
wait: Mycelium.operation({
|
|
485
|
+
replay: "unsafe",
|
|
486
|
+
description: ctx.config.waitDescription,
|
|
487
|
+
input: z.strictObject({
|
|
488
|
+
reason: z.string().min(1).max(1_000),
|
|
489
|
+
fallbackMinutes: z
|
|
490
|
+
.number()
|
|
491
|
+
.int()
|
|
492
|
+
.min(1)
|
|
493
|
+
.max(ctx.config.waitMaxMinutes)
|
|
494
|
+
.optional(),
|
|
495
|
+
}),
|
|
496
|
+
output: z.strictObject({
|
|
497
|
+
objective: toolObjective,
|
|
498
|
+
state: z.literal("active"),
|
|
499
|
+
reason: z.string(),
|
|
500
|
+
fallbackMinutes: z.number().int(),
|
|
501
|
+
}),
|
|
502
|
+
execute: ({ reason, fallbackMinutes }) => toolCall(ctx, "wait", Effect.gen(function* () {
|
|
503
|
+
const decoded = yield* Schema.decodeUnknownEffect(GoalReason)(reason).pipe(Effect.mapError(() => conflict("Wait reason is invalid")));
|
|
504
|
+
const goal = yield* currentGoal;
|
|
505
|
+
if (goal.state !== "active")
|
|
506
|
+
return yield* Effect.fail(conflict("Only an active Session goal can wait"));
|
|
507
|
+
if (goal.pending != null)
|
|
508
|
+
return yield* Effect.fail(conflict("This run already requested a goal terminal"));
|
|
509
|
+
const minutes = fallbackMinutes ?? ctx.config.waitDefaultMinutes;
|
|
510
|
+
yield* ctx.state.goals.put({
|
|
511
|
+
...goal,
|
|
512
|
+
// This run's settlement starts the wait.
|
|
513
|
+
trackedWorkId: workId,
|
|
514
|
+
waiting: {
|
|
515
|
+
reason: decoded,
|
|
516
|
+
fallbackMs: minutes * 60_000,
|
|
517
|
+
until: null,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
objective: goal.objective,
|
|
522
|
+
state: "active",
|
|
523
|
+
reason: decoded,
|
|
524
|
+
fallbackMinutes: minutes,
|
|
525
|
+
};
|
|
526
|
+
})),
|
|
527
|
+
}),
|
|
328
528
|
update_goal: Mycelium.operation({
|
|
329
529
|
replay: "unsafe",
|
|
330
530
|
description: ctx.config.updateDescription,
|
|
331
531
|
input: z.strictObject({
|
|
332
|
-
status: z.enum(["complete", "blocked"]),
|
|
532
|
+
status: z.enum(["complete", "blocked", "clear", "resume"]),
|
|
333
533
|
reason: z.string().min(1).max(1_000),
|
|
334
534
|
}),
|
|
335
535
|
output: z.strictObject({
|
|
336
536
|
objective: toolObjective,
|
|
337
|
-
state: z.enum(["complete", "blocked"]),
|
|
537
|
+
state: z.enum(["active", "complete", "blocked", "cleared"]),
|
|
338
538
|
reason: z.string(),
|
|
339
539
|
}),
|
|
340
540
|
execute: ({ status, reason }) => toolCall(ctx, "update_goal", Effect.gen(function* () {
|
|
341
541
|
const decoded = yield* Schema.decodeUnknownEffect(GoalReason)(reason).pipe(Effect.mapError(() => conflict("Goal reason is invalid")));
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
542
|
+
const goal = yield* currentGoal;
|
|
543
|
+
// A person's pause or stop is theirs: the model may only finish it.
|
|
544
|
+
if (status !== "complete" && heldByPerson(goal))
|
|
545
|
+
return yield* Effect.fail(conflict("A person paused or stopped this goal; only complete is available to you"));
|
|
546
|
+
if (status === "resume") {
|
|
547
|
+
const resumed = yield* modelResume(goal, workId);
|
|
548
|
+
yield* commitGoal(ctx, resumed);
|
|
549
|
+
yield* ctx.observe(sessionId);
|
|
550
|
+
return {
|
|
551
|
+
objective: goal.objective,
|
|
552
|
+
state: "active",
|
|
553
|
+
reason: decoded,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// Only a person discards a live goal; the model may clear a paused one.
|
|
557
|
+
if (status === "clear" && goal.state !== "paused")
|
|
558
|
+
return yield* Effect.fail(conflict("Only a paused Session goal can be cleared"));
|
|
559
|
+
const state = status === "clear" ? "cleared" : status;
|
|
347
560
|
yield* commitGoal(ctx, {
|
|
348
|
-
...
|
|
349
|
-
|
|
561
|
+
...goal,
|
|
562
|
+
// This run's settlement commits the provisional terminal.
|
|
563
|
+
trackedWorkId: workId,
|
|
564
|
+
pending: { state, reason: decoded },
|
|
350
565
|
});
|
|
351
|
-
return {
|
|
352
|
-
objective: found.value.objective,
|
|
353
|
-
state: status,
|
|
354
|
-
reason: decoded,
|
|
355
|
-
};
|
|
566
|
+
return { objective: goal.objective, state, reason: decoded };
|
|
356
567
|
})),
|
|
357
568
|
}),
|
|
358
569
|
},
|
|
359
570
|
});
|
|
360
571
|
}
|
|
572
|
+
/** A person's pause or Esc; only a person resumes, blocks or clears it. */
|
|
573
|
+
function heldByPerson(goal) {
|
|
574
|
+
return (goal.state === "paused" &&
|
|
575
|
+
(goal.pauseCause?.kind === "person" ||
|
|
576
|
+
goal.pauseCause?.kind === "cancelled"));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* The model resumes its own paused goal. The resuming run becomes the
|
|
580
|
+
* tracked work, so its settlement admits the next goal turn.
|
|
581
|
+
*/
|
|
582
|
+
function modelResume(goal, workId) {
|
|
583
|
+
if (goal.state !== "paused")
|
|
584
|
+
return Effect.fail(conflict("Session goal is not paused"));
|
|
585
|
+
return Effect.succeed({
|
|
586
|
+
...goal,
|
|
587
|
+
state: "active",
|
|
588
|
+
pauseCause: null,
|
|
589
|
+
pending: null,
|
|
590
|
+
stalledTurns: 0,
|
|
591
|
+
trackedWorkId: workId,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
361
594
|
function pauseGoal(ctx, sessionId) {
|
|
362
595
|
return changeActivity(ctx, sessionId, "paused").pipe(Effect.map(committed));
|
|
363
596
|
}
|
|
@@ -377,15 +610,22 @@ function setGoal(ctx, sessionId, input) {
|
|
|
377
610
|
const retried = current?.revisionRequestId === input.requestId;
|
|
378
611
|
if (!retried && (current?.revision ?? null) !== input.expectedRevision)
|
|
379
612
|
return yield* Effect.fail(conflict("Session goal changed; read it before replacing it"));
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
613
|
+
// Adopted work already carries the objective; anything else admits it.
|
|
614
|
+
const admitted = input.adoptCommandId === undefined
|
|
615
|
+
? yield* ctx.send({
|
|
616
|
+
sessionId,
|
|
617
|
+
when: "now",
|
|
618
|
+
prompt: {
|
|
619
|
+
text: input.objective,
|
|
620
|
+
requestId: input.requestId,
|
|
621
|
+
delivery: "followUp",
|
|
622
|
+
},
|
|
623
|
+
})
|
|
624
|
+
: {
|
|
625
|
+
_tag: "Accepted",
|
|
626
|
+
commandId: input.adoptCommandId,
|
|
627
|
+
delivery: retried ? "replay" : "new",
|
|
628
|
+
};
|
|
389
629
|
if (admitted._tag !== "Accepted")
|
|
390
630
|
return yield* Effect.fail(conflict("Session goal was not admitted"));
|
|
391
631
|
const accepted = {
|
|
@@ -414,12 +654,20 @@ function setGoal(ctx, sessionId, input) {
|
|
|
414
654
|
revisionRequestId: input.requestId,
|
|
415
655
|
trackedWorkId: admitted.commandId,
|
|
416
656
|
pauseCause: null,
|
|
657
|
+
waiting: null,
|
|
658
|
+
successor: null,
|
|
417
659
|
};
|
|
418
|
-
|
|
660
|
+
// Adopted work that already settled left the Session idle: start the
|
|
661
|
+
// first goal turn now. Otherwise its settlement does.
|
|
662
|
+
const started = input.adoptCommandId === undefined
|
|
663
|
+
? row
|
|
664
|
+
: yield* continueIfIdle(ctx, row, `goal-adopt:${input.requestId}`);
|
|
665
|
+
yield* commitGoal(ctx, started);
|
|
419
666
|
yield* ctx.observe(sessionId);
|
|
420
|
-
return { goal: committed(
|
|
667
|
+
return { goal: committed(started), accepted };
|
|
421
668
|
});
|
|
422
669
|
}
|
|
670
|
+
/** The one-line state the model prompt and attention summaries carry. */
|
|
423
671
|
function goalLine(goal) {
|
|
424
672
|
if (goal.state === "active")
|
|
425
673
|
return `Pursuing goal: ${goal.objective}`;
|
|
@@ -453,21 +701,15 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
453
701
|
if (!("action" in input)) {
|
|
454
702
|
if (current === undefined)
|
|
455
703
|
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");
|
|
704
|
+
return ask(goalMenuPrompt(committed(current), yield* Clock.currentTimeMillis), {}, "action");
|
|
468
705
|
}
|
|
469
706
|
switch (input.action) {
|
|
707
|
+
case "clear-ask":
|
|
708
|
+
// The menu's Clear: Keep is the focused answer to this destructive step.
|
|
709
|
+
return ask(clearConfirmation, { action: "clear" }, "confirmed");
|
|
470
710
|
case "clear":
|
|
711
|
+
if (input.confirmed === false)
|
|
712
|
+
return declined({ type: "info", message: "Kept the goal." });
|
|
471
713
|
// The chat's ending line confirms a clear; only an empty one says so.
|
|
472
714
|
return (yield* clearGoal(ctx, sessionId))
|
|
473
715
|
? { type: "done" }
|
|
@@ -483,7 +725,6 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
483
725
|
action: "set",
|
|
484
726
|
requestId: yield* freshRequestId,
|
|
485
727
|
expectedRevision: current?.revision ?? null,
|
|
486
|
-
confirmed: true,
|
|
487
728
|
}, "objective");
|
|
488
729
|
case "set": {
|
|
489
730
|
const retried = current?.revisionRequestId === input.requestId;
|
|
@@ -492,12 +733,7 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
492
733
|
if (replacing && input.confirmed === false)
|
|
493
734
|
return declined({ type: "info", message: "Kept the current goal." });
|
|
494
735
|
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
|
-
}, {
|
|
736
|
+
return ask(replaceConfirmation(committed(current), input.objective), {
|
|
501
737
|
action: "set",
|
|
502
738
|
objective: input.objective,
|
|
503
739
|
requestId: input.requestId,
|
|
@@ -556,69 +792,80 @@ export const goalCatalog = catalog([
|
|
|
556
792
|
},
|
|
557
793
|
},
|
|
558
794
|
]);
|
|
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
|
-
|
|
795
|
+
/** Botanical's Session goal; hosts with created-Agent facts include them in progress. */
|
|
796
|
+
export function goalExtensionFor(subagents) {
|
|
797
|
+
return (defineExtension({
|
|
798
|
+
id: goalContract.id,
|
|
799
|
+
version: "7.0.0",
|
|
800
|
+
})
|
|
801
|
+
.config(config)
|
|
802
|
+
// A command anchor cannot be converted into a historical Session position.
|
|
803
|
+
// Old preview state must be republished separately, never guessed here.
|
|
804
|
+
.state({ version: 7, migrations: {}, collections })
|
|
805
|
+
.define({
|
|
806
|
+
attention: true,
|
|
807
|
+
prompt: (ctx, { sessionId, origin }) => Effect.gen(function* () {
|
|
808
|
+
const goal = Option.getOrNull(yield* ctx.state.goals.get(sessionId));
|
|
809
|
+
const text = goal === null
|
|
810
|
+
? ctx.config.idle
|
|
811
|
+
: (goal.state === "active"
|
|
812
|
+
? ctx.config.guide
|
|
813
|
+
: ctx.config.idle) +
|
|
814
|
+
"\n" +
|
|
815
|
+
goalLine(committed(goal));
|
|
816
|
+
return [
|
|
817
|
+
{
|
|
818
|
+
name: "goal",
|
|
819
|
+
text: boundedPrompt(text, ctx.config.promptLimit),
|
|
820
|
+
},
|
|
821
|
+
...(goal?.state === "active" &&
|
|
822
|
+
origin?.extensionId === goalContract.id &&
|
|
823
|
+
origin.correlation !== null
|
|
824
|
+
? [
|
|
825
|
+
{
|
|
826
|
+
name: "goal-continuation",
|
|
827
|
+
text: boundedPrompt(ctx.config.continuation, ctx.config.promptLimit),
|
|
828
|
+
},
|
|
829
|
+
]
|
|
830
|
+
: []),
|
|
831
|
+
];
|
|
832
|
+
}),
|
|
833
|
+
catalog: goalCatalog,
|
|
834
|
+
projection: {
|
|
835
|
+
schema: goalContract.projection.schema,
|
|
836
|
+
read: (ctx, sessionId) => {
|
|
837
|
+
const goal = Option.getOrUndefined(ctx.state.goals.peek(sessionId));
|
|
838
|
+
const endings = Option.getOrUndefined(ctx.state.endings.peek(sessionId));
|
|
839
|
+
if (goal === undefined && endings === undefined)
|
|
840
|
+
return Option.none();
|
|
841
|
+
const ended = endings?.ended ?? [];
|
|
842
|
+
return Option.some({
|
|
843
|
+
goal: goal === undefined ? null : committed(goal),
|
|
844
|
+
ended,
|
|
845
|
+
omitted: (endings?.total ?? 0) - ended.length,
|
|
846
|
+
});
|
|
581
847
|
},
|
|
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
848
|
},
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
})
|
|
849
|
+
hooks: {
|
|
850
|
+
onSettled: (ctx, page) => Effect.forEach(page, (run) => settle(ctx, run, subagents), {
|
|
851
|
+
discard: true,
|
|
852
|
+
}),
|
|
853
|
+
onTimer: (ctx, timer) => recheckWaiting(ctx, timer, subagents),
|
|
854
|
+
},
|
|
855
|
+
tools: (ctx, { sessionId, workId }) => ctx.state.goals
|
|
856
|
+
.get(sessionId)
|
|
857
|
+
.pipe(Effect.map((found) => Option.some(goalModule(ctx, sessionId, workId, Option.isSome(found) ? found.value.revision : null)))),
|
|
858
|
+
commands: {
|
|
859
|
+
specs: goalContract.commands,
|
|
860
|
+
handlers: (ctx) => ({
|
|
861
|
+
goal: ({ sessionId, input }) => humanGoal(ctx, sessionId, input),
|
|
862
|
+
pause: ({ sessionId }) => pauseGoal(ctx, sessionId),
|
|
863
|
+
resume: ({ sessionId }) => resumeGoal(ctx, sessionId),
|
|
864
|
+
clear: ({ sessionId }) => clearGoal(ctx, sessionId).pipe(Effect.as({})),
|
|
865
|
+
set: ({ sessionId, input }) => setGoal(ctx, sessionId, input),
|
|
866
|
+
}),
|
|
867
|
+
},
|
|
868
|
+
}));
|
|
869
|
+
}
|
|
870
|
+
/** Ordinary roster when no created-Agent directory is available. */
|
|
871
|
+
export const goalExtension = goalExtensionFor();
|