@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
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, 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
|
|
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. On Local, an Ask raised during work another Agent sent waits on that sender first: it always parks, reads `waiting on <name>` instead of needing a person, and rides the child's reply to the sender as `waiting_on_you`. Only that sender's Session can answer it with `agents.answer`, or pass it to a person with `agents.escalate`; a person may always answer. An Ask the sender neither answers nor escalates goes to a person when the sender's turn ends, or after fifteen minutes. Hub sends every Ask to a person. |
|
|
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 footer view: one row per Agent this Session sent work to or created, with its live status (working, waiting on an Agent, needs you, idle, failed, unavailable). A row opens that Agent's conversation, a question that needs a person can be answered from its row, 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
|
|
@@ -112,9 +112,8 @@ paused and `null` while active. The closed kinds are `person`, `blocked`,
|
|
|
112
112
|
text, never a discriminator. Only blocked/stalled goals emit `goal-blocked` or
|
|
113
113
|
`goal-stalled` attention, kept out of the main inbox with `inbox: false`.
|
|
114
114
|
Questions and these two Goal kinds ask for a person in Herdr activity;
|
|
115
|
-
person-paused, cancelled and failed goals do not.
|
|
116
|
-
|
|
117
|
-
superseded representation.
|
|
115
|
+
person-paused, cancelled and failed goals do not. The current Goal state is
|
|
116
|
+
version 7. Superseded preview Goal rows are not migrated to this representation.
|
|
118
117
|
|
|
119
118
|
Loop tools are offered in every run. `loop.get`, `loop.start` and `loop.stop`
|
|
120
119
|
work from any run of the Session, and the model may start a loop itself.
|
|
@@ -269,9 +268,14 @@ expands the footer rows, per Session and in memory).
|
|
|
269
268
|
`source.read(sessionId, limit)` returns the Agents that Session sent work to or
|
|
270
269
|
created, newest first, with a count of the rest; `source.open` is the host's
|
|
271
270
|
existing Agent access. Each child's status is its owner's own Session state:
|
|
272
|
-
`running` is working,
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
`running` is working, `done` is idle and `errored` is failed. Attention that
|
|
272
|
+
waits on a person (a question, or an approval an extension raises) is
|
|
273
|
+
`needs you`; a question that waits on the Agent that sent the work is
|
|
274
|
+
`waiting on <name>`, calm until it is escalated, and counted apart in the
|
|
275
|
+
collapsed summary, which is amber only when a child needs a person. A
|
|
276
|
+
`needs you` question carries its text and one Answer action that runs the
|
|
277
|
+
question's own answer conversation. A child whose state cannot be read is
|
|
278
|
+
unavailable, never idle.
|
|
275
279
|
|
|
276
280
|
Rows refresh when a watcher reads the parent Session and they are at least a
|
|
277
281
|
second old. While a row is working or waiting, they poll every five seconds
|
package/dist/goal/contract.d.ts
CHANGED
|
@@ -224,6 +224,12 @@ export declare const goalContract: {
|
|
|
224
224
|
readonly expectedRevision: Schema.NullOr<Schema.String>;
|
|
225
225
|
/** Idempotency key for this setting command. */
|
|
226
226
|
readonly requestId: Schema.String;
|
|
227
|
+
/**
|
|
228
|
+
* Work already admitted in this Session that carries the objective,
|
|
229
|
+
* such as a delegated send. The goal adopts it instead of admitting
|
|
230
|
+
* the objective again; its settlement starts the goal turns.
|
|
231
|
+
*/
|
|
232
|
+
readonly adoptCommandId: Schema.optionalKey<Schema.String>;
|
|
227
233
|
}>;
|
|
228
234
|
/** Return the goal and its admission receipt. */
|
|
229
235
|
readonly output: Schema.Struct<{
|
package/dist/goal/contract.js
CHANGED
|
@@ -154,6 +154,12 @@ export const goalContract = {
|
|
|
154
154
|
expectedRevision: Schema.NullOr(Schema.String),
|
|
155
155
|
/** Idempotency key for this setting command. */
|
|
156
156
|
requestId: RequestId,
|
|
157
|
+
/**
|
|
158
|
+
* Work already admitted in this Session that carries the objective,
|
|
159
|
+
* such as a delegated send. The goal adopts it instead of admitting
|
|
160
|
+
* the objective again; its settlement starts the goal turns.
|
|
161
|
+
*/
|
|
162
|
+
adoptCommandId: Schema.optionalKey(RequestId),
|
|
157
163
|
}),
|
|
158
164
|
/** Return the goal and its admission receipt. */
|
|
159
165
|
output: Schema.Struct({
|
package/dist/goal/host.js
CHANGED
|
@@ -14,14 +14,18 @@ import { clearConfirmation, goalMenuPrompt, replaceConfirmation, } from "./promp
|
|
|
14
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.";
|
|
15
15
|
/** Hashed prose and policy: every constant goal reads comes from here. */
|
|
16
16
|
const config = {
|
|
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.",
|
|
18
|
-
moduleDescription: "Session goal tools: create_goal({objective}), get_goal({}), update_goal({status:'complete'|'blocked'|'clear'|'resume', 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}). " +
|
|
19
19
|
rules,
|
|
20
|
-
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.",
|
|
21
21
|
getDescription: "Read the current goal. A requested terminal state remains provisional until successful settlement.",
|
|
22
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
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,
|
|
24
27
|
waitingSummary: "Waiting on sub-agent",
|
|
28
|
+
waitingOnSummary: "Waiting",
|
|
25
29
|
promptLimit: 1_024,
|
|
26
30
|
stallTurns: 3,
|
|
27
31
|
stallReason: "Stalled: no progress for 3 turns",
|
|
@@ -61,6 +65,17 @@ const GoalRow = Schema.Struct({
|
|
|
61
65
|
reason: GoalReason,
|
|
62
66
|
})),
|
|
63
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)),
|
|
64
79
|
/** Request that set this revision; its retry replays. */
|
|
65
80
|
revisionRequestId: Schema.String,
|
|
66
81
|
/** Latest work whose settlement advances this goal. */
|
|
@@ -127,6 +142,8 @@ function paused(goal, kind, detail) {
|
|
|
127
142
|
state: "paused",
|
|
128
143
|
pauseCause: { kind, detail },
|
|
129
144
|
pending: null,
|
|
145
|
+
waiting: null,
|
|
146
|
+
successor: null,
|
|
130
147
|
};
|
|
131
148
|
}
|
|
132
149
|
/**
|
|
@@ -167,6 +184,7 @@ function changeActivity(ctx, sessionId, target) {
|
|
|
167
184
|
state: "active",
|
|
168
185
|
pauseCause: null,
|
|
169
186
|
stalledTurns: 0,
|
|
187
|
+
waiting: null,
|
|
170
188
|
}
|
|
171
189
|
: paused(goal, "person", null);
|
|
172
190
|
yield* commitGoal(ctx, changed);
|
|
@@ -190,7 +208,9 @@ function continueIfIdle(ctx, goal, requestId) {
|
|
|
190
208
|
})
|
|
191
209
|
.pipe(Effect.map((admitted) => admitted._tag === "Accepted"
|
|
192
210
|
? { ...goal, trackedWorkId: admitted.commandId }
|
|
193
|
-
: goal), Effect.catchTag("AdmissionError", (error) =>
|
|
211
|
+
: goal), Effect.catchTag("AdmissionError", (error) => error.reason === "archived"
|
|
212
|
+
? Clock.currentTimeMillis.pipe(Effect.flatMap((now) => ctx.timers.set(waitTimerKey(goal.sessionId), now + ctx.config.waitCheckMs)), Effect.as(goal))
|
|
213
|
+
: Effect.succeed(paused(goal, "failed", error.message))));
|
|
194
214
|
}
|
|
195
215
|
/**
|
|
196
216
|
* Advance one Session's goal from one settled run. Any successful run in
|
|
@@ -225,26 +245,77 @@ function settle(ctx, run, subagents) {
|
|
|
225
245
|
yield* commitGoal(ctx, paused(goal, "no-terminal", ctx.config.noTerminalReason));
|
|
226
246
|
return yield* ctx.observe(run.sessionId);
|
|
227
247
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
const goalTurn = run.origin?.extensionId === goalContract.id;
|
|
249
|
+
// A live child command or a question or approval a person has yet to
|
|
250
|
+
// answer is a legitimate wait, whether or not this turn polled.
|
|
251
|
+
const waitingForChild = goalTurn &&
|
|
252
|
+
(subagents?.hasLiveChildCommand(run.sessionId) === true ||
|
|
253
|
+
(yield* ctx.attention.awaitsPerson(run.sessionId)));
|
|
254
|
+
const waiting = goal.waiting ?? null;
|
|
255
|
+
// The run that called goal.wait fixes the fallback instant as it settles.
|
|
256
|
+
const requested = waiting !== null &&
|
|
257
|
+
waiting.until === null &&
|
|
258
|
+
run.workId === goal.trackedWorkId;
|
|
259
|
+
// Any other run that settles while a wait is armed wakes the goal.
|
|
260
|
+
const woken = waiting !== null && waiting.until !== null;
|
|
231
261
|
const tracked = {
|
|
232
262
|
...goal,
|
|
233
263
|
trackedWorkId: run.workId,
|
|
234
|
-
stalledTurns:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
stalledTurns: requested
|
|
265
|
+
? goal.stalledTurns
|
|
266
|
+
: run.origin?.extensionId === goalContract.id &&
|
|
267
|
+
run.origin.correlation === goal.revision
|
|
268
|
+
? run.toolCalls === 0
|
|
269
|
+
? goal.stalledTurns + 1
|
|
270
|
+
: 0
|
|
271
|
+
: 0,
|
|
240
272
|
};
|
|
241
273
|
const pending = goal.trackedWorkId === run.workId ? goal.pending : null;
|
|
242
|
-
if (pending?.state === "complete")
|
|
243
|
-
yield*
|
|
244
|
-
|
|
245
|
-
|
|
274
|
+
if (pending?.state === "complete" || pending?.state === "cleared") {
|
|
275
|
+
yield* ctx.timers.cancel(waitTimerKey(run.sessionId));
|
|
276
|
+
yield* finishGoal(ctx, tracked, pending.state === "complete" ? "achieved" : "cleared", pending.reason);
|
|
277
|
+
if (goal.successor != null) {
|
|
278
|
+
const next = {
|
|
279
|
+
sessionId: run.sessionId,
|
|
280
|
+
objective: goal.successor,
|
|
281
|
+
state: "active",
|
|
282
|
+
startedAt: yield* Clock.currentTimeMillis,
|
|
283
|
+
pending: null,
|
|
284
|
+
stalledTurns: 0,
|
|
285
|
+
revision: `run:${run.workId}`,
|
|
286
|
+
revisionRequestId: `run:${run.workId}`,
|
|
287
|
+
trackedWorkId: run.workId,
|
|
288
|
+
pauseCause: null,
|
|
289
|
+
waiting: null,
|
|
290
|
+
successor: null,
|
|
291
|
+
};
|
|
292
|
+
yield* commitGoal(ctx, yield* continueIfIdle(ctx, next, `goal-continue:${run.workId}`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
246
295
|
else if (pending?.state === "blocked")
|
|
247
296
|
yield* commitGoal(ctx, paused(tracked, "blocked", pending.reason));
|
|
297
|
+
else if (goal.state === "active" && requested) {
|
|
298
|
+
// goal.wait: no continuation and no stall; the next settled run or the
|
|
299
|
+
// fallback timer wakes the goal.
|
|
300
|
+
const now = yield* Clock.currentTimeMillis;
|
|
301
|
+
const until = now + waiting.fallbackMs;
|
|
302
|
+
yield* commitGoal(ctx, {
|
|
303
|
+
...tracked,
|
|
304
|
+
waiting: { ...waiting, until },
|
|
305
|
+
});
|
|
306
|
+
yield* ctx.attention.set(run.sessionId, {
|
|
307
|
+
id: "waiting-goal",
|
|
308
|
+
kind: "goal-waiting",
|
|
309
|
+
inbox: false,
|
|
310
|
+
summary: `${ctx.config.waitingOnSummary}: ${waiting.reason}`.slice(0, 200),
|
|
311
|
+
since: now,
|
|
312
|
+
});
|
|
313
|
+
yield* ctx.timers.set(waitTimerKey(run.sessionId), until);
|
|
314
|
+
}
|
|
315
|
+
else if (goal.state === "active" && woken) {
|
|
316
|
+
yield* ctx.timers.cancel(waitTimerKey(run.sessionId));
|
|
317
|
+
yield* commitGoal(ctx, yield* continueIfIdle(ctx, { ...tracked, waiting: null }, `goal-continue:${run.workId}`));
|
|
318
|
+
}
|
|
248
319
|
else if (goal.state === "active" &&
|
|
249
320
|
!waitingForChild &&
|
|
250
321
|
tracked.stalledTurns >= ctx.config.stallTurns)
|
|
@@ -288,7 +359,15 @@ function recheckWaiting(ctx, timer, subagents) {
|
|
|
288
359
|
const found = yield* ctx.state.goals.get(sessionId);
|
|
289
360
|
if (Option.isNone(found) || found.value.state !== "active")
|
|
290
361
|
return;
|
|
291
|
-
|
|
362
|
+
const armed = found.value.waiting ?? null;
|
|
363
|
+
if (armed !== null) {
|
|
364
|
+
// goal.wait's fallback: a re-armed wait ignores a stale firing.
|
|
365
|
+
if (armed.until === null || armed.until > timer.firedAt)
|
|
366
|
+
return;
|
|
367
|
+
return yield* commitGoal(ctx, yield* continueIfIdle(ctx, { ...found.value, waiting: null, stalledTurns: 0 }, `goal-wait:${found.value.trackedWorkId}`)).pipe(Effect.andThen(ctx.observe(sessionId)));
|
|
368
|
+
}
|
|
369
|
+
if (subagents?.hasLiveChildCommand(sessionId) === true ||
|
|
370
|
+
(yield* ctx.attention.awaitsPerson(sessionId)))
|
|
292
371
|
return yield* ctx.timers.set(timer.key, timer.firedAt + ctx.config.waitCheckMs);
|
|
293
372
|
yield* commitGoal(ctx, yield* continueIfIdle(ctx, found.value, `goal-wait:${found.value.trackedWorkId}`));
|
|
294
373
|
yield* ctx.observe(sessionId);
|
|
@@ -349,8 +428,21 @@ function goalModule(ctx, sessionId, workId, initialRevision) {
|
|
|
349
428
|
}),
|
|
350
429
|
execute: ({ objective }) => toolCall(ctx, "create_goal", Effect.gen(function* () {
|
|
351
430
|
const decoded = yield* Schema.decodeUnknownEffect(Objective)(objective).pipe(Effect.mapError(() => conflict("Goal objective is invalid")));
|
|
352
|
-
|
|
353
|
-
|
|
431
|
+
const existing = yield* ctx.state.goals.get(sessionId);
|
|
432
|
+
if (Option.isSome(existing)) {
|
|
433
|
+
// A goal this run already finished (complete, or clear of a
|
|
434
|
+
// paused one) hands over to the replacement at settlement.
|
|
435
|
+
const retiring = existing.value.trackedWorkId === workId &&
|
|
436
|
+
(existing.value.pending?.state === "complete" ||
|
|
437
|
+
existing.value.pending?.state === "cleared");
|
|
438
|
+
if (!retiring)
|
|
439
|
+
return yield* Effect.fail(conflict("A Session goal is already active or paused; complete it first, or only a person replaces it"));
|
|
440
|
+
yield* ctx.state.goals.put({
|
|
441
|
+
...existing.value,
|
|
442
|
+
successor: decoded,
|
|
443
|
+
});
|
|
444
|
+
return { objective: decoded, state: "active" };
|
|
445
|
+
}
|
|
354
446
|
yield* commitGoal(ctx, {
|
|
355
447
|
sessionId,
|
|
356
448
|
objective: decoded,
|
|
@@ -362,6 +454,8 @@ function goalModule(ctx, sessionId, workId, initialRevision) {
|
|
|
362
454
|
revisionRequestId: `run:${workId}`,
|
|
363
455
|
trackedWorkId: workId,
|
|
364
456
|
pauseCause: null,
|
|
457
|
+
waiting: null,
|
|
458
|
+
successor: null,
|
|
365
459
|
});
|
|
366
460
|
yield* ctx.observe(sessionId);
|
|
367
461
|
return { objective: decoded, state: "active" };
|
|
@@ -389,6 +483,50 @@ function goalModule(ctx, sessionId, workId, initialRevision) {
|
|
|
389
483
|
null,
|
|
390
484
|
}))),
|
|
391
485
|
}),
|
|
486
|
+
wait: Mycelium.operation({
|
|
487
|
+
replay: "unsafe",
|
|
488
|
+
description: ctx.config.waitDescription,
|
|
489
|
+
input: z.strictObject({
|
|
490
|
+
reason: z.string().min(1).max(1_000),
|
|
491
|
+
fallbackMinutes: z
|
|
492
|
+
.number()
|
|
493
|
+
.int()
|
|
494
|
+
.min(1)
|
|
495
|
+
.max(ctx.config.waitMaxMinutes)
|
|
496
|
+
.optional(),
|
|
497
|
+
}),
|
|
498
|
+
output: z.strictObject({
|
|
499
|
+
objective: toolObjective,
|
|
500
|
+
state: z.literal("active"),
|
|
501
|
+
reason: z.string(),
|
|
502
|
+
fallbackMinutes: z.number().int(),
|
|
503
|
+
}),
|
|
504
|
+
execute: ({ reason, fallbackMinutes }) => toolCall(ctx, "wait", Effect.gen(function* () {
|
|
505
|
+
const decoded = yield* Schema.decodeUnknownEffect(GoalReason)(reason).pipe(Effect.mapError(() => conflict("Wait reason is invalid")));
|
|
506
|
+
const goal = yield* currentGoal;
|
|
507
|
+
if (goal.state !== "active")
|
|
508
|
+
return yield* Effect.fail(conflict("Only an active Session goal can wait"));
|
|
509
|
+
if (goal.pending != null)
|
|
510
|
+
return yield* Effect.fail(conflict("This run already requested a goal terminal"));
|
|
511
|
+
const minutes = fallbackMinutes ?? ctx.config.waitDefaultMinutes;
|
|
512
|
+
yield* ctx.state.goals.put({
|
|
513
|
+
...goal,
|
|
514
|
+
// This run's settlement starts the wait.
|
|
515
|
+
trackedWorkId: workId,
|
|
516
|
+
waiting: {
|
|
517
|
+
reason: decoded,
|
|
518
|
+
fallbackMs: minutes * 60_000,
|
|
519
|
+
until: null,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
return {
|
|
523
|
+
objective: goal.objective,
|
|
524
|
+
state: "active",
|
|
525
|
+
reason: decoded,
|
|
526
|
+
fallbackMinutes: minutes,
|
|
527
|
+
};
|
|
528
|
+
})),
|
|
529
|
+
}),
|
|
392
530
|
update_goal: Mycelium.operation({
|
|
393
531
|
replay: "unsafe",
|
|
394
532
|
description: ctx.config.updateDescription,
|
|
@@ -474,15 +612,22 @@ function setGoal(ctx, sessionId, input) {
|
|
|
474
612
|
const retried = current?.revisionRequestId === input.requestId;
|
|
475
613
|
if (!retried && (current?.revision ?? null) !== input.expectedRevision)
|
|
476
614
|
return yield* Effect.fail(conflict("Session goal changed; read it before replacing it"));
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
615
|
+
// Adopted work already carries the objective; anything else admits it.
|
|
616
|
+
const admitted = input.adoptCommandId === undefined
|
|
617
|
+
? yield* ctx.send({
|
|
618
|
+
sessionId,
|
|
619
|
+
when: "now",
|
|
620
|
+
prompt: {
|
|
621
|
+
text: input.objective,
|
|
622
|
+
requestId: input.requestId,
|
|
623
|
+
delivery: "followUp",
|
|
624
|
+
},
|
|
625
|
+
})
|
|
626
|
+
: {
|
|
627
|
+
_tag: "Accepted",
|
|
628
|
+
commandId: input.adoptCommandId,
|
|
629
|
+
delivery: retried ? "replay" : "new",
|
|
630
|
+
};
|
|
486
631
|
if (admitted._tag !== "Accepted")
|
|
487
632
|
return yield* Effect.fail(conflict("Session goal was not admitted"));
|
|
488
633
|
const accepted = {
|
|
@@ -511,10 +656,17 @@ function setGoal(ctx, sessionId, input) {
|
|
|
511
656
|
revisionRequestId: input.requestId,
|
|
512
657
|
trackedWorkId: admitted.commandId,
|
|
513
658
|
pauseCause: null,
|
|
659
|
+
waiting: null,
|
|
660
|
+
successor: null,
|
|
514
661
|
};
|
|
515
|
-
|
|
662
|
+
// Adopted work that already settled left the Session idle: start the
|
|
663
|
+
// first goal turn now. Otherwise its settlement does.
|
|
664
|
+
const started = input.adoptCommandId === undefined
|
|
665
|
+
? row
|
|
666
|
+
: yield* continueIfIdle(ctx, row, `goal-adopt:${input.requestId}`);
|
|
667
|
+
yield* commitGoal(ctx, started);
|
|
516
668
|
yield* ctx.observe(sessionId);
|
|
517
|
-
return { goal: committed(
|
|
669
|
+
return { goal: committed(started), accepted };
|
|
518
670
|
});
|
|
519
671
|
}
|
|
520
672
|
/** The one-line state the model prompt and attention summaries carry. */
|
package/dist/loop/contract.d.ts
CHANGED
|
@@ -93,12 +93,146 @@ export declare const LoopProjection: Schema.Struct<{
|
|
|
93
93
|
readonly consecutiveFailures: Schema.Int;
|
|
94
94
|
}>;
|
|
95
95
|
}>;
|
|
96
|
-
/** Session-owned
|
|
96
|
+
/** Session-owned current-Loop controls and Agent-scoped identified-Loop controls. */
|
|
97
97
|
export declare const loopContract: {
|
|
98
98
|
/** Stable extension identity. */
|
|
99
99
|
readonly id: string & import("effect/Brand").Brand<"ExtensionId">;
|
|
100
|
-
/** Loop operations admitted by
|
|
100
|
+
/** Loop operations admitted by their Agent or Session owner. */
|
|
101
101
|
readonly commands: {
|
|
102
|
+
/** Read an Agent's bounded Loop page, including each destination Session. */
|
|
103
|
+
readonly list: {
|
|
104
|
+
/** The Agent owns the collection of its Sessions' Loops. */
|
|
105
|
+
readonly scope: "agent";
|
|
106
|
+
/** Page strictly after a Session identity, in identity order. */
|
|
107
|
+
readonly input: Schema.Struct<{
|
|
108
|
+
/** Exclusive Session cursor. */
|
|
109
|
+
readonly after: Schema.optionalKey<Schema.String>;
|
|
110
|
+
/** Maximum number of Loop rows to read. */
|
|
111
|
+
readonly limit: Schema.optionalKey<Schema.Int>;
|
|
112
|
+
}>;
|
|
113
|
+
/** Public Loop values with Session targets, never private scheduling state. */
|
|
114
|
+
readonly output: Schema.$Array<Schema.Struct<{
|
|
115
|
+
/** Session that owns this identified Loop. */
|
|
116
|
+
readonly sessionId: Schema.String;
|
|
117
|
+
/** Identity of the loop whose occurrences are being tracked. */
|
|
118
|
+
readonly loopId: Schema.String;
|
|
119
|
+
/** Whether a person or the model started this loop. */
|
|
120
|
+
readonly origin: Schema.Literals<readonly ["human", "model"]>;
|
|
121
|
+
/** Configured expiry timestamp, or null when absent. */
|
|
122
|
+
readonly expiresAt: Schema.NullOr<Schema.Int>;
|
|
123
|
+
/** Original instruction repeated for every occurrence. */
|
|
124
|
+
readonly prompt: Schema.String;
|
|
125
|
+
/** Whether the model or a fixed cron cadence schedules occurrences. */
|
|
126
|
+
readonly mode: Schema.Literals<readonly ["self_paced", "fixed_interval"]>;
|
|
127
|
+
/** Cadence of a fixed-interval loop; null when the model paces it. */
|
|
128
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
129
|
+
/** Cron expression for the fixed cadence. */
|
|
130
|
+
readonly expression: Schema.String;
|
|
131
|
+
/** IANA time zone in which the expression is evaluated. */
|
|
132
|
+
readonly timeZone: Schema.String;
|
|
133
|
+
}>>;
|
|
134
|
+
/** Current loop lifecycle state. */
|
|
135
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "finished", "failed", "cancelled"]>;
|
|
136
|
+
/** Latest accepted command associated with this loop. */
|
|
137
|
+
readonly commandId: Schema.String;
|
|
138
|
+
/** The model's stated reason for the next self-paced occurrence. */
|
|
139
|
+
readonly reason: Schema.NullOr<Schema.String>;
|
|
140
|
+
/** When the next occurrence is due, if one is scheduled. */
|
|
141
|
+
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
142
|
+
/** Visible explanation when loop work failed. */
|
|
143
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
144
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
145
|
+
readonly consecutiveFailures: Schema.Int;
|
|
146
|
+
}>>;
|
|
147
|
+
};
|
|
148
|
+
/** Pause an identified Loop; refuse a stale target after replacement. */
|
|
149
|
+
readonly pauseLoop: {
|
|
150
|
+
/** The Agent owns the identified Loop and its Session. */
|
|
151
|
+
readonly scope: "agent";
|
|
152
|
+
/** The listed Session and Loop identities must both still match. */
|
|
153
|
+
readonly input: Schema.Struct<{
|
|
154
|
+
/** Session containing the identified Loop. */
|
|
155
|
+
readonly sessionId: Schema.String;
|
|
156
|
+
/** Exact Loop identity, not whichever Loop later replaces it. */
|
|
157
|
+
readonly loopId: Schema.String;
|
|
158
|
+
}>;
|
|
159
|
+
/** Return the paused Loop without cancelling accepted work. */
|
|
160
|
+
readonly output: Schema.Struct<{
|
|
161
|
+
/** Identity of the loop whose occurrences are being tracked. */
|
|
162
|
+
readonly loopId: Schema.String;
|
|
163
|
+
/** Whether a person or the model started this loop. */
|
|
164
|
+
readonly origin: Schema.Literals<readonly ["human", "model"]>;
|
|
165
|
+
/** Configured expiry timestamp, or null when absent. */
|
|
166
|
+
readonly expiresAt: Schema.NullOr<Schema.Int>;
|
|
167
|
+
/** Original instruction repeated for every occurrence. */
|
|
168
|
+
readonly prompt: Schema.String;
|
|
169
|
+
/** Whether the model or a fixed cron cadence schedules occurrences. */
|
|
170
|
+
readonly mode: Schema.Literals<readonly ["self_paced", "fixed_interval"]>;
|
|
171
|
+
/** Cadence of a fixed-interval loop; null when the model paces it. */
|
|
172
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
173
|
+
/** Cron expression for the fixed cadence. */
|
|
174
|
+
readonly expression: Schema.String;
|
|
175
|
+
/** IANA time zone in which the expression is evaluated. */
|
|
176
|
+
readonly timeZone: Schema.String;
|
|
177
|
+
}>>;
|
|
178
|
+
/** Current loop lifecycle state. */
|
|
179
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "finished", "failed", "cancelled"]>;
|
|
180
|
+
/** Latest accepted command associated with this loop. */
|
|
181
|
+
readonly commandId: Schema.String;
|
|
182
|
+
/** The model's stated reason for the next self-paced occurrence. */
|
|
183
|
+
readonly reason: Schema.NullOr<Schema.String>;
|
|
184
|
+
/** When the next occurrence is due, if one is scheduled. */
|
|
185
|
+
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
186
|
+
/** Visible explanation when loop work failed. */
|
|
187
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
188
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
189
|
+
readonly consecutiveFailures: Schema.Int;
|
|
190
|
+
}>;
|
|
191
|
+
};
|
|
192
|
+
/** Cancel an identified Loop's future occurrences, retaining accepted work. */
|
|
193
|
+
readonly stopLoop: {
|
|
194
|
+
/** The Agent owns the identified Loop and its Session. */
|
|
195
|
+
readonly scope: "agent";
|
|
196
|
+
/** The listed Session and Loop identities must both still match. */
|
|
197
|
+
readonly input: Schema.Struct<{
|
|
198
|
+
/** Session containing the identified Loop. */
|
|
199
|
+
readonly sessionId: Schema.String;
|
|
200
|
+
/** Exact Loop identity, not whichever Loop later replaces it. */
|
|
201
|
+
readonly loopId: Schema.String;
|
|
202
|
+
}>;
|
|
203
|
+
/** Return the cancelled Loop; its Session and history remain. */
|
|
204
|
+
readonly output: Schema.Struct<{
|
|
205
|
+
/** Identity of the loop whose occurrences are being tracked. */
|
|
206
|
+
readonly loopId: Schema.String;
|
|
207
|
+
/** Whether a person or the model started this loop. */
|
|
208
|
+
readonly origin: Schema.Literals<readonly ["human", "model"]>;
|
|
209
|
+
/** Configured expiry timestamp, or null when absent. */
|
|
210
|
+
readonly expiresAt: Schema.NullOr<Schema.Int>;
|
|
211
|
+
/** Original instruction repeated for every occurrence. */
|
|
212
|
+
readonly prompt: Schema.String;
|
|
213
|
+
/** Whether the model or a fixed cron cadence schedules occurrences. */
|
|
214
|
+
readonly mode: Schema.Literals<readonly ["self_paced", "fixed_interval"]>;
|
|
215
|
+
/** Cadence of a fixed-interval loop; null when the model paces it. */
|
|
216
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
217
|
+
/** Cron expression for the fixed cadence. */
|
|
218
|
+
readonly expression: Schema.String;
|
|
219
|
+
/** IANA time zone in which the expression is evaluated. */
|
|
220
|
+
readonly timeZone: Schema.String;
|
|
221
|
+
}>>;
|
|
222
|
+
/** Current loop lifecycle state. */
|
|
223
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "finished", "failed", "cancelled"]>;
|
|
224
|
+
/** Latest accepted command associated with this loop. */
|
|
225
|
+
readonly commandId: Schema.String;
|
|
226
|
+
/** The model's stated reason for the next self-paced occurrence. */
|
|
227
|
+
readonly reason: Schema.NullOr<Schema.String>;
|
|
228
|
+
/** When the next occurrence is due, if one is scheduled. */
|
|
229
|
+
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
230
|
+
/** Visible explanation when loop work failed. */
|
|
231
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
232
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
233
|
+
readonly consecutiveFailures: Schema.Int;
|
|
234
|
+
}>;
|
|
235
|
+
};
|
|
102
236
|
/** Open or answer the human loop controls. */
|
|
103
237
|
readonly loop: {
|
|
104
238
|
/** The Session owns this loop. */
|
package/dist/loop/contract.js
CHANGED
|
@@ -8,6 +8,7 @@ import { CatalogStep } from "../internal/catalog.js";
|
|
|
8
8
|
*/
|
|
9
9
|
const Prompt = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256 * 1024)));
|
|
10
10
|
const RequestId = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(200)));
|
|
11
|
+
const SessionId = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
|
|
11
12
|
/** A named cron cadence evaluated in one IANA time zone. */
|
|
12
13
|
export const LoopCron = Schema.Struct({
|
|
13
14
|
/** Cron expression for the fixed cadence. */
|
|
@@ -93,12 +94,54 @@ export const LoopProjection = Schema.Struct({
|
|
|
93
94
|
/** Loop currently visible to Session clients. */
|
|
94
95
|
loop: Loop,
|
|
95
96
|
});
|
|
96
|
-
|
|
97
|
+
const LoopTarget = Schema.Struct({
|
|
98
|
+
/** Session containing the identified Loop. */
|
|
99
|
+
sessionId: SessionId,
|
|
100
|
+
/** Exact Loop identity, not whichever Loop later replaces it. */
|
|
101
|
+
loopId: Loop.fields.loopId,
|
|
102
|
+
});
|
|
103
|
+
/** Session-owned current-Loop controls and Agent-scoped identified-Loop controls. */
|
|
97
104
|
export const loopContract = {
|
|
98
105
|
/** Stable extension identity. */
|
|
99
106
|
id: ExtensionId.make("botanical-session-loop"),
|
|
100
|
-
/** Loop operations admitted by
|
|
107
|
+
/** Loop operations admitted by their Agent or Session owner. */
|
|
101
108
|
commands: {
|
|
109
|
+
/** Read an Agent's bounded Loop page, including each destination Session. */
|
|
110
|
+
list: {
|
|
111
|
+
/** The Agent owns the collection of its Sessions' Loops. */
|
|
112
|
+
scope: "agent",
|
|
113
|
+
/** Page strictly after a Session identity, in identity order. */
|
|
114
|
+
input: Schema.Struct({
|
|
115
|
+
/** Exclusive Session cursor. */
|
|
116
|
+
after: Schema.optionalKey(SessionId),
|
|
117
|
+
/** Maximum number of Loop rows to read. */
|
|
118
|
+
limit: Schema.optionalKey(Schema.Int.pipe(Schema.check(Schema.isBetween({ minimum: 1, maximum: 64 })))),
|
|
119
|
+
}),
|
|
120
|
+
/** Public Loop values with Session targets, never private scheduling state. */
|
|
121
|
+
output: Schema.Array(Schema.Struct({
|
|
122
|
+
...Loop.fields,
|
|
123
|
+
/** Session that owns this identified Loop. */
|
|
124
|
+
sessionId: SessionId,
|
|
125
|
+
})).pipe(Schema.check(Schema.isMaxLength(64))),
|
|
126
|
+
},
|
|
127
|
+
/** Pause an identified Loop; refuse a stale target after replacement. */
|
|
128
|
+
pauseLoop: {
|
|
129
|
+
/** The Agent owns the identified Loop and its Session. */
|
|
130
|
+
scope: "agent",
|
|
131
|
+
/** The listed Session and Loop identities must both still match. */
|
|
132
|
+
input: LoopTarget,
|
|
133
|
+
/** Return the paused Loop without cancelling accepted work. */
|
|
134
|
+
output: Loop,
|
|
135
|
+
},
|
|
136
|
+
/** Cancel an identified Loop's future occurrences, retaining accepted work. */
|
|
137
|
+
stopLoop: {
|
|
138
|
+
/** The Agent owns the identified Loop and its Session. */
|
|
139
|
+
scope: "agent",
|
|
140
|
+
/** The listed Session and Loop identities must both still match. */
|
|
141
|
+
input: LoopTarget,
|
|
142
|
+
/** Return the cancelled Loop; its Session and history remain. */
|
|
143
|
+
output: Loop,
|
|
144
|
+
},
|
|
102
145
|
/** Open or answer the human loop controls. */
|
|
103
146
|
loop: {
|
|
104
147
|
/** The Session owns this loop. */
|