@fungi.computer/shiitake-extensions 0.1.0 → 0.2.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/dist/goal/host.js +18 -11
- package/dist/home/host.d.ts +5 -0
- package/dist/home/host.js +2 -1
- package/dist/loop/contract.d.ts +16 -0
- package/dist/loop/contract.js +2 -0
- package/dist/loop/host.js +73 -19
- package/dist/roster/conversation-agents.js +2 -2
- package/docs/api/loop/contract.md +32 -30
- package/package.json +4 -4
package/dist/goal/host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mycelium } from "@fungi.computer/mycelium";
|
|
1
|
+
import { CapabilityFailed, Mycelium, } from "@fungi.computer/mycelium";
|
|
2
2
|
import { defineCollection, defineExtension, ExtensionCommandError, ExtensionStop, } from "@fungi.computer/shiitake/extensions";
|
|
3
3
|
import * as Cause from "effect/Cause";
|
|
4
4
|
import * as Clock from "effect/Clock";
|
|
@@ -245,17 +245,24 @@ function settle(ctx, run) {
|
|
|
245
245
|
yield* ctx.observe(run.sessionId);
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
|
-
/** Run one tool transaction and
|
|
249
|
-
function toolCall(ctx, body) {
|
|
248
|
+
/** Run one tool transaction and preserve its owner-admitted diagnostic. */
|
|
249
|
+
function toolCall(ctx, operation, body) {
|
|
250
250
|
return Effect.runPromiseExit(ctx.transact(body)).then((exit) => {
|
|
251
251
|
if (Exit.isSuccess(exit))
|
|
252
252
|
return exit.value;
|
|
253
253
|
const failure = Cause.squash(exit.cause);
|
|
254
|
-
return Promise.reject(new
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
: "
|
|
254
|
+
return Promise.reject(new CapabilityFailed({
|
|
255
|
+
namespace: "goal",
|
|
256
|
+
operation,
|
|
257
|
+
cause: {
|
|
258
|
+
kind: "OperationFailed",
|
|
259
|
+
message: failure instanceof ExtensionStop
|
|
260
|
+
? failure.reason
|
|
261
|
+
: failure instanceof Error
|
|
262
|
+
? failure.message
|
|
263
|
+
: "Goal tool failed",
|
|
264
|
+
},
|
|
265
|
+
}));
|
|
259
266
|
});
|
|
260
267
|
}
|
|
261
268
|
// Mycelium's maintained operation interface takes Zod for model-tool I/O.
|
|
@@ -276,7 +283,7 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
276
283
|
objective: toolObjective,
|
|
277
284
|
state: z.literal("active"),
|
|
278
285
|
}),
|
|
279
|
-
execute: ({ objective }) => toolCall(ctx, Effect.gen(function* () {
|
|
286
|
+
execute: ({ objective }) => toolCall(ctx, "create_goal", Effect.gen(function* () {
|
|
280
287
|
const decoded = yield* Schema.decodeUnknownEffect(Objective)(objective).pipe(Effect.mapError(() => conflict("Goal objective is invalid")));
|
|
281
288
|
if (Option.isSome(yield* ctx.state.goals.get(sessionId)))
|
|
282
289
|
return yield* Effect.fail(conflict("A Session goal is already active or paused; only a person replaces it"));
|
|
@@ -308,7 +315,7 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
308
315
|
}),
|
|
309
316
|
z.null(),
|
|
310
317
|
]),
|
|
311
|
-
execute: () => toolCall(ctx, ctx.state.goals.get(sessionId).pipe(Effect.map((found) => Option.isNone(found)
|
|
318
|
+
execute: () => toolCall(ctx, "get_goal", ctx.state.goals.get(sessionId).pipe(Effect.map((found) => Option.isNone(found)
|
|
312
319
|
? null
|
|
313
320
|
: {
|
|
314
321
|
objective: found.value.objective,
|
|
@@ -330,7 +337,7 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
330
337
|
state: z.enum(["complete", "blocked"]),
|
|
331
338
|
reason: z.string(),
|
|
332
339
|
}),
|
|
333
|
-
execute: ({ status, reason }) => toolCall(ctx, Effect.gen(function* () {
|
|
340
|
+
execute: ({ status, reason }) => toolCall(ctx, "update_goal", Effect.gen(function* () {
|
|
334
341
|
const decoded = yield* Schema.decodeUnknownEffect(GoalReason)(reason).pipe(Effect.mapError(() => conflict("Goal reason is invalid")));
|
|
335
342
|
const found = yield* ctx.state.goals.get(sessionId);
|
|
336
343
|
if (Option.isNone(found) ||
|
package/dist/home/host.d.ts
CHANGED
|
@@ -14,8 +14,13 @@ export declare const homeGrant: ExtensionGrantV0;
|
|
|
14
14
|
/** Agent-scope choice with per-run tools and prompt over one captured table. */
|
|
15
15
|
export declare function homeExtension(options: {
|
|
16
16
|
computers: {
|
|
17
|
+
/** Read the table inside the synchronous Home choice transaction. */
|
|
17
18
|
read: (context: Readonly<{
|
|
18
19
|
sessionId: string;
|
|
19
20
|
}>) => readonly HomeComputer[];
|
|
21
|
+
/** Optional host preparation before run-scoped Files capture. */
|
|
22
|
+
prepare?: (context: Readonly<{
|
|
23
|
+
sessionId: string;
|
|
24
|
+
}>) => Promise<void>;
|
|
20
25
|
};
|
|
21
26
|
}): RosteredExtensionV0;
|
package/dist/home/host.js
CHANGED
|
@@ -51,7 +51,8 @@ export function homeExtension(options) {
|
|
|
51
51
|
schema: Schema.NullOr(Home),
|
|
52
52
|
read: (ctx) => Option.some(Option.getOrNull(ctx.state.choice.peek("default"))),
|
|
53
53
|
},
|
|
54
|
-
files: (ctx, run) => {
|
|
54
|
+
files: async (ctx, run) => {
|
|
55
|
+
await options.computers.prepare?.(run);
|
|
55
56
|
const computers = [...options.computers.read(run)];
|
|
56
57
|
const choice = ctx.state.update((state) => {
|
|
57
58
|
const recorded = Option.getOrNull(state.choice.get("default"));
|
package/dist/loop/contract.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export declare const Loop: Schema.Struct<{
|
|
|
35
35
|
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
36
36
|
/** Visible explanation when loop work failed. */
|
|
37
37
|
readonly lastError: Schema.NullOr<Schema.String>;
|
|
38
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
39
|
+
readonly consecutiveFailures: Schema.Int;
|
|
38
40
|
}>;
|
|
39
41
|
/** One Session's loop. */
|
|
40
42
|
export type Loop = Schema.Schema.Type<typeof Loop>;
|
|
@@ -85,6 +87,8 @@ export declare const LoopProjection: Schema.Struct<{
|
|
|
85
87
|
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
86
88
|
/** Visible explanation when loop work failed. */
|
|
87
89
|
readonly lastError: Schema.NullOr<Schema.String>;
|
|
90
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
91
|
+
readonly consecutiveFailures: Schema.Int;
|
|
88
92
|
}>;
|
|
89
93
|
}>;
|
|
90
94
|
/** Session-owned loop controls and their visible state. */
|
|
@@ -154,6 +158,8 @@ export declare const loopContract: {
|
|
|
154
158
|
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
155
159
|
/** Visible explanation when loop work failed. */
|
|
156
160
|
readonly lastError: Schema.NullOr<Schema.String>;
|
|
161
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
162
|
+
readonly consecutiveFailures: Schema.Int;
|
|
157
163
|
}>;
|
|
158
164
|
/** Admission receipt for the start command. */
|
|
159
165
|
readonly accepted: Schema.Struct<{
|
|
@@ -210,6 +216,8 @@ export declare const loopContract: {
|
|
|
210
216
|
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
211
217
|
/** Visible explanation when loop work failed. */
|
|
212
218
|
readonly lastError: Schema.NullOr<Schema.String>;
|
|
219
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
220
|
+
readonly consecutiveFailures: Schema.Int;
|
|
213
221
|
}>;
|
|
214
222
|
/** Admission receipt for the start command. */
|
|
215
223
|
readonly accepted: Schema.Struct<{
|
|
@@ -255,6 +263,8 @@ export declare const loopContract: {
|
|
|
255
263
|
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
256
264
|
/** Visible explanation when loop work failed. */
|
|
257
265
|
readonly lastError: Schema.NullOr<Schema.String>;
|
|
266
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
267
|
+
readonly consecutiveFailures: Schema.Int;
|
|
258
268
|
}>;
|
|
259
269
|
};
|
|
260
270
|
/** Resume the current paused loop. */
|
|
@@ -292,6 +302,8 @@ export declare const loopContract: {
|
|
|
292
302
|
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
293
303
|
/** Visible explanation when loop work failed. */
|
|
294
304
|
readonly lastError: Schema.NullOr<Schema.String>;
|
|
305
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
306
|
+
readonly consecutiveFailures: Schema.Int;
|
|
295
307
|
}>;
|
|
296
308
|
};
|
|
297
309
|
/** Stop future occurrences of the current loop. */
|
|
@@ -329,6 +341,8 @@ export declare const loopContract: {
|
|
|
329
341
|
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
330
342
|
/** Visible explanation when loop work failed. */
|
|
331
343
|
readonly lastError: Schema.NullOr<Schema.String>;
|
|
344
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
345
|
+
readonly consecutiveFailures: Schema.Int;
|
|
332
346
|
}>;
|
|
333
347
|
};
|
|
334
348
|
};
|
|
@@ -365,6 +379,8 @@ export declare const loopContract: {
|
|
|
365
379
|
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
366
380
|
/** Visible explanation when loop work failed. */
|
|
367
381
|
readonly lastError: Schema.NullOr<Schema.String>;
|
|
382
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
383
|
+
readonly consecutiveFailures: Schema.Int;
|
|
368
384
|
}>;
|
|
369
385
|
}>;
|
|
370
386
|
};
|
package/dist/loop/contract.js
CHANGED
|
@@ -45,6 +45,8 @@ export const Loop = Schema.Struct({
|
|
|
45
45
|
nextDueAt: Schema.NullOr(Schema.Int),
|
|
46
46
|
/** Visible explanation when loop work failed. */
|
|
47
47
|
lastError: Schema.NullOr(Schema.String),
|
|
48
|
+
/** Consecutive failed work outcomes; successful work resets this to zero. */
|
|
49
|
+
consecutiveFailures: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
48
50
|
});
|
|
49
51
|
const StartResult = Schema.Struct({
|
|
50
52
|
/** Newly started loop. */
|
package/dist/loop/host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mycelium } from "@fungi.computer/mycelium";
|
|
1
|
+
import { CapabilityFailed, Mycelium, } from "@fungi.computer/mycelium";
|
|
2
2
|
import { defineCollection, defineExtension, ExtensionCommandError, ExtensionStop, } from "@fungi.computer/shiitake/extensions";
|
|
3
3
|
import * as Cause from "effect/Cause";
|
|
4
4
|
import * as Clock from "effect/Clock";
|
|
@@ -12,7 +12,7 @@ import { ask, catalog, done, freshRequestId } from "../internal/catalog.js";
|
|
|
12
12
|
import { nextOccurrence } from "../internal/cron.js";
|
|
13
13
|
import { Loop, LoopCommandInput, loopContract } from "./contract.js";
|
|
14
14
|
/** The rules the model follows; the describe page and a live loop's prompt share them. */
|
|
15
|
-
const rules = "You may start loops yourself; one live loop per Session. schedule_next is allowed only in the active self-paced loop's own run, clamped to 60–3600 seconds. Omitting it ends that loop. stop works in any Session run. Human pause/resume/stop and Esc remain user controls.";
|
|
15
|
+
const rules = "You may start loops yourself; one live loop per Session. Failed loop ticks retry with backoff; loop.get shows consecutiveFailures, lastError, and nextDueAt. schedule_next is allowed only in the active self-paced loop's own run, clamped to 60–3600 seconds. Omitting it ends that loop. stop works in any Session run. Human pause/resume/stop and Esc remain user controls.";
|
|
16
16
|
/** Hashed prose and policy: every constant loop reads comes from here. */
|
|
17
17
|
const config = {
|
|
18
18
|
minDelaySeconds: 60,
|
|
@@ -38,6 +38,7 @@ const LoopRow = Schema.Struct({
|
|
|
38
38
|
delaySeconds: Schema.Number,
|
|
39
39
|
reason: Schema.String,
|
|
40
40
|
})),
|
|
41
|
+
consecutiveFailures: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.withDecodingDefaultKey(Effect.succeed(0))),
|
|
41
42
|
});
|
|
42
43
|
const collections = {
|
|
43
44
|
loops: defineCollection(LoopRow, {
|
|
@@ -61,6 +62,7 @@ function projected(row) {
|
|
|
61
62
|
reason: row.reason,
|
|
62
63
|
nextDueAt: row.nextDueAt,
|
|
63
64
|
lastError: row.lastError,
|
|
65
|
+
consecutiveFailures: row.consecutiveFailures,
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
function commitLoop(ctx, row) {
|
|
@@ -127,6 +129,7 @@ function start(ctx, sessionId, input, cron, origin = "human") {
|
|
|
127
129
|
lastError: null,
|
|
128
130
|
startRequestId: input.requestId,
|
|
129
131
|
requested: null,
|
|
132
|
+
consecutiveFailures: 0,
|
|
130
133
|
};
|
|
131
134
|
const stored = cron === null
|
|
132
135
|
? row
|
|
@@ -150,6 +153,8 @@ function requireLoop(ctx, sessionId) {
|
|
|
150
153
|
}
|
|
151
154
|
/** Where a resumed loop picks up: its cadence, or its recorded next instant. */
|
|
152
155
|
function resumeAt(loop, now) {
|
|
156
|
+
if (loop.consecutiveFailures > 0)
|
|
157
|
+
return Effect.succeed(loop.nextDueAt === null ? null : Math.max(now, loop.nextDueAt));
|
|
153
158
|
return loop.cron === null
|
|
154
159
|
? Effect.succeed(loop.nextDueAt)
|
|
155
160
|
: nextOccurrence(loop.cron, now);
|
|
@@ -165,6 +170,27 @@ function settle(ctx, run) {
|
|
|
165
170
|
return;
|
|
166
171
|
if (run.workId !== loop.commandId)
|
|
167
172
|
return;
|
|
173
|
+
if (run.outcome === "failed") {
|
|
174
|
+
const consecutiveFailures = Math.min(Number.MAX_SAFE_INTEGER, loop.consecutiveFailures + 1);
|
|
175
|
+
const backoffExponent = Math.min(consecutiveFailures - 1, 5);
|
|
176
|
+
const delayMs = Math.min(15 * 60_000, 30_000 * 2 ** backoffExponent);
|
|
177
|
+
const now = yield* Clock.currentTimeMillis;
|
|
178
|
+
const dueAt = now + delayMs;
|
|
179
|
+
const retry = {
|
|
180
|
+
...loop,
|
|
181
|
+
state: loop.state,
|
|
182
|
+
requested: null,
|
|
183
|
+
reason: null,
|
|
184
|
+
nextDueAt: dueAt,
|
|
185
|
+
lastError: "Loop work ended with outcome failed.",
|
|
186
|
+
consecutiveFailures,
|
|
187
|
+
};
|
|
188
|
+
if (loop.state === "paused")
|
|
189
|
+
yield* commitLoop(ctx, retry);
|
|
190
|
+
else
|
|
191
|
+
yield* arm(ctx, retry, dueAt);
|
|
192
|
+
return yield* ctx.observe(loop.sessionId);
|
|
193
|
+
}
|
|
168
194
|
if (run.outcome !== "completed" || run.terminal !== "completed") {
|
|
169
195
|
yield* ctx.timers.cancel(loop.sessionId);
|
|
170
196
|
yield* commitLoop(ctx, {
|
|
@@ -176,12 +202,22 @@ function settle(ctx, run) {
|
|
|
176
202
|
lastError: run.outcome === "completed"
|
|
177
203
|
? "Completed loop work has no exact terminal."
|
|
178
204
|
: `Loop work ended with outcome ${run.outcome}.`,
|
|
205
|
+
consecutiveFailures: loop.consecutiveFailures,
|
|
179
206
|
});
|
|
180
207
|
return yield* ctx.observe(loop.sessionId);
|
|
181
208
|
}
|
|
182
209
|
// A fixed interval loop's cadence is armed independently of its runs.
|
|
183
|
-
if (loop.mode === "fixed_interval")
|
|
210
|
+
if (loop.mode === "fixed_interval") {
|
|
211
|
+
if (loop.consecutiveFailures !== 0 || loop.lastError !== null) {
|
|
212
|
+
yield* commitLoop(ctx, {
|
|
213
|
+
...loop,
|
|
214
|
+
consecutiveFailures: 0,
|
|
215
|
+
lastError: null,
|
|
216
|
+
});
|
|
217
|
+
yield* ctx.observe(loop.sessionId);
|
|
218
|
+
}
|
|
184
219
|
return;
|
|
220
|
+
}
|
|
185
221
|
// Omitting schedule_next during the run ends a self-paced loop.
|
|
186
222
|
if (loop.requested?.workId !== run.workId) {
|
|
187
223
|
yield* commitLoop(ctx, {
|
|
@@ -189,6 +225,8 @@ function settle(ctx, run) {
|
|
|
189
225
|
state: "finished",
|
|
190
226
|
reason: "no next wake requested",
|
|
191
227
|
nextDueAt: null,
|
|
228
|
+
lastError: null,
|
|
229
|
+
consecutiveFailures: 0,
|
|
192
230
|
});
|
|
193
231
|
return yield* ctx.observe(loop.sessionId);
|
|
194
232
|
}
|
|
@@ -198,6 +236,8 @@ function settle(ctx, run) {
|
|
|
198
236
|
reason: loop.requested.reason,
|
|
199
237
|
requested: null,
|
|
200
238
|
nextDueAt: now + loop.requested.delaySeconds * 1_000,
|
|
239
|
+
lastError: null,
|
|
240
|
+
consecutiveFailures: 0,
|
|
201
241
|
};
|
|
202
242
|
// A paused loop keeps the instant; resume arms it.
|
|
203
243
|
if (loop.state === "paused")
|
|
@@ -273,17 +313,24 @@ function fire(ctx, sessionId, dueAt, now) {
|
|
|
273
313
|
yield* ctx.observe(sessionId);
|
|
274
314
|
});
|
|
275
315
|
}
|
|
276
|
-
/** Run one tool transaction and
|
|
277
|
-
function toolCall(ctx, body) {
|
|
316
|
+
/** Run one tool transaction and preserve its owner-admitted diagnostic. */
|
|
317
|
+
function toolCall(ctx, operation, body) {
|
|
278
318
|
return Effect.runPromiseExit(ctx.transact(body)).then((exit) => {
|
|
279
319
|
if (Exit.isSuccess(exit))
|
|
280
320
|
return exit.value;
|
|
281
321
|
const failure = Cause.squash(exit.cause);
|
|
282
|
-
return Promise.reject(new
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
: "
|
|
322
|
+
return Promise.reject(new CapabilityFailed({
|
|
323
|
+
namespace: "loop",
|
|
324
|
+
operation,
|
|
325
|
+
cause: {
|
|
326
|
+
kind: "OperationFailed",
|
|
327
|
+
message: failure instanceof ExtensionStop
|
|
328
|
+
? failure.reason
|
|
329
|
+
: failure instanceof Error
|
|
330
|
+
? failure.message
|
|
331
|
+
: "Loop tool failed",
|
|
332
|
+
},
|
|
333
|
+
}));
|
|
287
334
|
});
|
|
288
335
|
}
|
|
289
336
|
// Mycelium's maintained operation interface takes Zod for model-tool I/O.
|
|
@@ -305,6 +352,7 @@ const toolLoop = z.strictObject({
|
|
|
305
352
|
reason: z.string().nullable(),
|
|
306
353
|
nextDueAt: z.number().nullable(),
|
|
307
354
|
lastError: z.string().nullable(),
|
|
355
|
+
consecutiveFailures: z.number().int().nonnegative(),
|
|
308
356
|
});
|
|
309
357
|
/** The self-paced `schedule_next` tool for exactly the loop's current run. */
|
|
310
358
|
function loopModule(ctx, sessionId, workId) {
|
|
@@ -318,7 +366,7 @@ function loopModule(ctx, sessionId, workId) {
|
|
|
318
366
|
description: "Get the current Session loop and its next scheduled wake.",
|
|
319
367
|
input: z.strictObject({}),
|
|
320
368
|
output: toolLoop.nullable(),
|
|
321
|
-
execute: () => toolCall(ctx, ctx.state.loops
|
|
369
|
+
execute: () => toolCall(ctx, "get", ctx.state.loops
|
|
322
370
|
.get(sessionId)
|
|
323
371
|
.pipe(Effect.map((found) => Option.isSome(found) ? projected(found.value) : null))),
|
|
324
372
|
}),
|
|
@@ -343,7 +391,7 @@ function loopModule(ctx, sessionId, workId) {
|
|
|
343
391
|
]),
|
|
344
392
|
}),
|
|
345
393
|
output: toolLoop,
|
|
346
|
-
execute: ({ prompt, cadence }) => toolCall(ctx, Effect.gen(function* () {
|
|
394
|
+
execute: ({ prompt, cadence }) => toolCall(ctx, "start", Effect.gen(function* () {
|
|
347
395
|
const requestId = yield* freshRequestId;
|
|
348
396
|
const cron = cadence.kind === "self_paced"
|
|
349
397
|
? null
|
|
@@ -362,7 +410,7 @@ function loopModule(ctx, sessionId, workId) {
|
|
|
362
410
|
description: "Stop this Session's loop from any run, with a visible reason. Cancel its next scheduled wake.",
|
|
363
411
|
input: z.strictObject({ reason: z.string().trim().min(1).max(1_000) }),
|
|
364
412
|
output: toolLoop,
|
|
365
|
-
execute: ({ reason }) => toolCall(ctx, stopLoop(ctx, sessionId, reason)),
|
|
413
|
+
execute: ({ reason }) => toolCall(ctx, "stop", stopLoop(ctx, sessionId, reason)),
|
|
366
414
|
}),
|
|
367
415
|
schedule_next: Mycelium.operation({
|
|
368
416
|
replay: "unsafe",
|
|
@@ -373,13 +421,16 @@ function loopModule(ctx, sessionId, workId) {
|
|
|
373
421
|
dueAt: z.number(),
|
|
374
422
|
reason: z.string().min(1).max(4_000),
|
|
375
423
|
}),
|
|
376
|
-
execute: ({ delaySeconds, reason }) => toolCall(ctx, Effect.gen(function* () {
|
|
424
|
+
execute: ({ delaySeconds, reason }) => toolCall(ctx, "schedule_next", Effect.gen(function* () {
|
|
377
425
|
const found = yield* ctx.state.loops.get(sessionId);
|
|
378
|
-
if (Option.isNone(found)
|
|
379
|
-
|
|
426
|
+
if (Option.isNone(found))
|
|
427
|
+
return yield* Effect.fail(conflict("schedule_next only works inside this loop's own tick run"));
|
|
428
|
+
if (found.value.state === "failed")
|
|
429
|
+
return yield* Effect.fail(conflict("loop is failed; start a new one with loop.start"));
|
|
430
|
+
if (found.value.state !== "active" ||
|
|
380
431
|
found.value.mode !== "self_paced" ||
|
|
381
432
|
found.value.commandId !== workId)
|
|
382
|
-
return yield* Effect.fail(conflict("
|
|
433
|
+
return yield* Effect.fail(conflict("schedule_next only works inside this loop's own tick run"));
|
|
383
434
|
const clamped = Math.max(ctx.config.minDelaySeconds, Math.min(ctx.config.maxDelaySeconds, delaySeconds));
|
|
384
435
|
yield* commitLoop(ctx, {
|
|
385
436
|
...found.value,
|
|
@@ -575,10 +626,13 @@ export const loopCatalog = catalog([
|
|
|
575
626
|
/** Durable Session loop implementation with prompts, controls and scheduled occurrences. */
|
|
576
627
|
export const loopExtension = defineExtension({
|
|
577
628
|
id: loopContract.id,
|
|
578
|
-
version: "
|
|
629
|
+
version: "4.0.0",
|
|
579
630
|
})
|
|
580
631
|
.config(config)
|
|
581
|
-
.state({
|
|
632
|
+
.state({
|
|
633
|
+
version: 3,
|
|
634
|
+
collections,
|
|
635
|
+
})
|
|
582
636
|
.define({
|
|
583
637
|
prompt: (ctx, { sessionId }) => ctx.state.loops.get(sessionId).pipe(Effect.map((found) => [
|
|
584
638
|
{
|
|
@@ -35,9 +35,9 @@ export function createConversationAgents(storage) {
|
|
|
35
35
|
function created(parentSessionId, agentId) {
|
|
36
36
|
sql().exec("INSERT OR IGNORE INTO oyster_conversation_agents(parent_session_id, agent_id, session_id, at) VALUES (?, ?, NULL, ?)", parentSessionId, agentId, Date.now());
|
|
37
37
|
}
|
|
38
|
-
/** A send
|
|
38
|
+
/** A send may not know its Session until the recipient accepts it. */
|
|
39
39
|
function sent(parentSessionId, agentId, sessionId) {
|
|
40
|
-
sql().exec("INSERT INTO oyster_conversation_agents(parent_session_id, agent_id, session_id, at) VALUES (?, ?, ?, ?) ON CONFLICT(parent_session_id, agent_id) DO UPDATE SET session_id = excluded.session_id, at = excluded.at", parentSessionId, agentId, sessionId, Date.now());
|
|
40
|
+
sql().exec("INSERT INTO oyster_conversation_agents(parent_session_id, agent_id, session_id, at) VALUES (?, ?, ?, ?) ON CONFLICT(parent_session_id, agent_id) DO UPDATE SET session_id = excluded.session_id, at = excluded.at", parentSessionId, agentId, sessionId ?? null, Date.now());
|
|
41
41
|
}
|
|
42
42
|
/** One conversation's directory: its accepted creates and sends are recorded. */
|
|
43
43
|
function recording(parentSessionId, directory) {
|
|
@@ -55,6 +55,7 @@ const Loop: Struct<{
|
|
|
55
55
|
reason: NullOr<String>;
|
|
56
56
|
nextDueAt: NullOr<Int>;
|
|
57
57
|
lastError: NullOr<String>;
|
|
58
|
+
consecutiveFailures: Int;
|
|
58
59
|
}>;
|
|
59
60
|
```
|
|
60
61
|
|
|
@@ -109,6 +110,7 @@ const LoopProjection: Struct<{
|
|
|
109
110
|
reason: NullOr<String>;
|
|
110
111
|
nextDueAt: NullOr<Int>;
|
|
111
112
|
lastError: NullOr<String>;
|
|
113
|
+
consecutiveFailures: Int;
|
|
112
114
|
}>;
|
|
113
115
|
}>;
|
|
114
116
|
```
|
|
@@ -127,33 +129,33 @@ Session-owned loop controls and their visible state.
|
|
|
127
129
|
|
|
128
130
|
#### Type Declaration
|
|
129
131
|
|
|
130
|
-
| Name | Type
|
|
131
|
-
| --------------------------------------------- |
|
|
132
|
-
| <a id="property-id"></a> `id` | `string` & `Brand`\<`"ExtensionId"`\>
|
|
133
|
-
| <a id="property-commands"></a> `commands` | `object`
|
|
134
|
-
| `commands.loop` | `object`
|
|
135
|
-
| `commands.loop.scope` | `"session"`
|
|
136
|
-
| `commands.loop.input` | `Union`\<readonly \[`Struct`\<\{ `action`: `Literal`\<`"start"`\>; `text`: `String`; `requestId`: `String`; \}\>, `Struct`\<\{ `action`: `Literals`\<readonly \[`"status"`, `"pause"`, `"resume"`, `"stop"`, `"new"`\]\>; \}\>, `Struct`\<\{ \}\>\]\>
|
|
137
|
-
| `commands.loop.output` | `declare`\<`WhistleCatalogStepV1`, `WhistleCatalogStepV1`\>
|
|
138
|
-
| `commands.startSelfPaced` | `object`
|
|
139
|
-
| `commands.startSelfPaced.scope` | `"session"`
|
|
140
|
-
| `commands.startSelfPaced.input` | `Struct`\<\{ `prompt`: `String`; `requestId`: `String`; \}\>
|
|
141
|
-
| `commands.startSelfPaced.output` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | `StartResult` | Return the loop and admission receipt. |
|
|
142
|
-
| `commands.startFixedInterval` | `object`
|
|
143
|
-
| `commands.startFixedInterval.scope` | `"session"`
|
|
144
|
-
| `commands.startFixedInterval.input` | `Struct`\<\{ `expression`: `String`; `timeZone`: `String`; `prompt`: `String`; `requestId`: `String`; \}\>
|
|
145
|
-
| `commands.startFixedInterval.output` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | `StartResult` | Return the loop and admission receipt. |
|
|
146
|
-
| `commands.pause` | `object`
|
|
147
|
-
| `commands.pause.scope` | `"session"`
|
|
148
|
-
| `commands.pause.input` | `Struct`\<\{ \}\>
|
|
149
|
-
| `commands.pause.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; \}\> | `Loop` | Return the paused loop. |
|
|
150
|
-
| `commands.resume` | `object`
|
|
151
|
-
| `commands.resume.scope` | `"session"`
|
|
152
|
-
| `commands.resume.input` | `Struct`\<\{ \}\>
|
|
153
|
-
| `commands.resume.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; \}\> | `Loop` | Return the resumed loop. |
|
|
154
|
-
| `commands.stop` | `object`
|
|
155
|
-
| `commands.stop.scope` | `"session"`
|
|
156
|
-
| `commands.stop.input` | `Struct`\<\{ \}\>
|
|
157
|
-
| `commands.stop.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; \}\> | `Loop` | Return the stopped loop. |
|
|
158
|
-
| <a id="property-projection"></a> `projection` | `object`
|
|
159
|
-
| `projection.schema` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; \}\>; \}\> | `LoopProjection` | Decoder for projected loop state. |
|
|
132
|
+
| Name | Type | Default value | Description |
|
|
133
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ---------------------------------------------------------- |
|
|
134
|
+
| <a id="property-id"></a> `id` | `string` & `Brand`\<`"ExtensionId"`\> | - | Stable extension identity. |
|
|
135
|
+
| <a id="property-commands"></a> `commands` | `object` | - | Loop operations admitted by the Session owner. |
|
|
136
|
+
| `commands.loop` | `object` | - | Open or answer the human loop controls. |
|
|
137
|
+
| `commands.loop.scope` | `"session"` | `"session"` | The Session owns this loop. |
|
|
138
|
+
| `commands.loop.input` | `Union`\<readonly \[`Struct`\<\{ `action`: `Literal`\<`"start"`\>; `text`: `String`; `requestId`: `String`; \}\>, `Struct`\<\{ `action`: `Literals`\<readonly \[`"status"`, `"pause"`, `"resume"`, `"stop"`, `"new"`\]\>; \}\>, `Struct`\<\{ \}\>\]\> | `LoopCommandInput` | Decode the selected human action. |
|
|
139
|
+
| `commands.loop.output` | `declare`\<`WhistleCatalogStepV1`, `WhistleCatalogStepV1`\> | `CatalogStep` | Return the server-owned conversation step. |
|
|
140
|
+
| `commands.startSelfPaced` | `object` | - | Start a loop whose next occurrence is chosen by the model. |
|
|
141
|
+
| `commands.startSelfPaced.scope` | `"session"` | `"session"` | The Session owns this loop. |
|
|
142
|
+
| `commands.startSelfPaced.input` | `Struct`\<\{ `prompt`: `String`; `requestId`: `String`; \}\> | - | Decode the repeated prompt and command identity. |
|
|
143
|
+
| `commands.startSelfPaced.output` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | `StartResult` | Return the loop and admission receipt. |
|
|
144
|
+
| `commands.startFixedInterval` | `object` | - | Start a loop with a fixed cron cadence. |
|
|
145
|
+
| `commands.startFixedInterval.scope` | `"session"` | `"session"` | The Session owns this loop. |
|
|
146
|
+
| `commands.startFixedInterval.input` | `Struct`\<\{ `expression`: `String`; `timeZone`: `String`; `prompt`: `String`; `requestId`: `String`; \}\> | - | Decode the prompt, cadence and command identity. |
|
|
147
|
+
| `commands.startFixedInterval.output` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | `StartResult` | Return the loop and admission receipt. |
|
|
148
|
+
| `commands.pause` | `object` | - | Suspend the current loop. |
|
|
149
|
+
| `commands.pause.scope` | `"session"` | `"session"` | The Session owns this loop. |
|
|
150
|
+
| `commands.pause.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
|
|
151
|
+
| `commands.pause.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the paused loop. |
|
|
152
|
+
| `commands.resume` | `object` | - | Resume the current paused loop. |
|
|
153
|
+
| `commands.resume.scope` | `"session"` | `"session"` | The Session owns this loop. |
|
|
154
|
+
| `commands.resume.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
|
|
155
|
+
| `commands.resume.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the resumed loop. |
|
|
156
|
+
| `commands.stop` | `object` | - | Stop future occurrences of the current loop. |
|
|
157
|
+
| `commands.stop.scope` | `"session"` | `"session"` | The Session owns this loop. |
|
|
158
|
+
| `commands.stop.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
|
|
159
|
+
| `commands.stop.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the stopped loop. |
|
|
160
|
+
| <a id="property-projection"></a> `projection` | `object` | - | Current loop visible to Session clients. |
|
|
161
|
+
| `projection.schema` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; \}\> | `LoopProjection` | Decoder for projected loop state. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/shiitake-extensions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"author": "Levi <levi@technomancy.dev>",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Botanical's product Shiitake extensions: goal, loop and Almanac on the generic extension surface",
|
|
@@ -108,9 +108,9 @@
|
|
|
108
108
|
"dependencies": {
|
|
109
109
|
"@fungi.computer/almanac": "0.2.0",
|
|
110
110
|
"@fungi.computer/cairn": "0.1.0",
|
|
111
|
-
"@fungi.computer/knapsack": "0.
|
|
112
|
-
"@fungi.computer/mycelium": "0.2.
|
|
113
|
-
"@fungi.computer/shiitake": "0.
|
|
111
|
+
"@fungi.computer/knapsack": "0.3.0",
|
|
112
|
+
"@fungi.computer/mycelium": "0.2.2",
|
|
113
|
+
"@fungi.computer/shiitake": "0.4.0",
|
|
114
114
|
"@fungi.computer/watchdog": "0.1.0",
|
|
115
115
|
"@fungi.computer/whistle": "0.1.0",
|
|
116
116
|
"effect": "4.0.2",
|