@fungi.computer/shiitake-extensions 0.0.1 → 0.1.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 +43 -13
- package/dist/almanac/contract.d.ts +94 -0
- package/dist/almanac/contract.js +71 -5
- package/dist/goal/client.d.ts +5 -2
- package/dist/goal/client.js +66 -72
- package/dist/goal/contract.d.ts +181 -64
- package/dist/goal/contract.js +102 -12
- package/dist/goal/host.js +188 -113
- package/dist/home/contract.d.ts +18 -0
- package/dist/home/contract.js +24 -3
- package/dist/home/host.d.ts +3 -0
- package/dist/home/host.js +1 -2
- package/dist/home/routing.js +13 -24
- package/dist/internal/history.d.ts +16 -0
- package/dist/internal/history.js +8 -0
- package/dist/loop/client.d.ts +1 -1
- package/dist/loop/client.js +28 -34
- package/dist/loop/contract.d.ts +113 -84
- package/dist/loop/contract.js +77 -9
- package/dist/loop/host.d.ts +1 -0
- package/dist/loop/host.js +17 -30
- package/dist/plan/client.d.ts +1 -1
- package/dist/plan/contract.d.ts +33 -0
- package/dist/plan/contract.js +28 -3
- package/dist/plan/host.js +12 -7
- package/dist/question/client.d.ts +1 -1
- package/dist/question/contract.d.ts +158 -0
- package/dist/question/contract.js +92 -7
- package/dist/question/host.js +34 -20
- package/dist/roster/conversation-agents.d.ts +31 -0
- package/dist/roster/conversation-agents.js +114 -0
- package/dist/roster.js +1 -0
- package/dist/skills/host.d.ts +1 -0
- package/dist/skills/host.js +22 -17
- package/dist/skills/source.d.ts +34 -0
- package/dist/subagents/client.d.ts +1 -1
- package/dist/subagents/client.js +43 -12
- package/dist/subagents/contract.d.ts +46 -8
- package/dist/subagents/contract.js +38 -8
- package/dist/subagents/host.d.ts +20 -8
- package/dist/subagents/host.js +58 -12
- package/docs/api/README.md +35 -0
- package/docs/api/almanac/contract.md +90 -0
- package/docs/api/almanac/host.md +19 -0
- package/docs/api/goal/client.md +26 -0
- package/docs/api/goal/contract.md +247 -0
- package/docs/api/goal/host.md +29 -0
- package/docs/api/home/contract.md +58 -0
- package/docs/api/home/host.md +55 -0
- package/docs/api/loop/client.md +25 -0
- package/docs/api/loop/contract.md +159 -0
- package/docs/api/loop/host.md +30 -0
- package/docs/api/plan/client.md +26 -0
- package/docs/api/plan/contract.md +163 -0
- package/docs/api/plan/host.md +19 -0
- package/docs/api/question/client.md +25 -0
- package/docs/api/question/contract.md +377 -0
- package/docs/api/question/host.md +19 -0
- package/docs/api/roster/conversation-agents.md +80 -0
- package/docs/api/roster.md +40 -0
- package/docs/api/skills/host.md +39 -0
- package/docs/api/skills/source.md +104 -0
- package/docs/api/subagents/client.md +26 -0
- package/docs/api/subagents/contract.md +131 -0
- package/docs/api/subagents/host.md +115 -0
- package/package.json +14 -8
package/dist/goal/host.js
CHANGED
|
@@ -1,26 +1,44 @@
|
|
|
1
|
-
import * as Clock from "effect/Clock";
|
|
2
|
-
import { appendHistory, emptyHistory, historySchema, } from "../internal/history.js";
|
|
3
1
|
import { Mycelium } from "@fungi.computer/mycelium";
|
|
4
2
|
import { defineCollection, defineExtension, ExtensionCommandError, ExtensionStop, } from "@fungi.computer/shiitake/extensions";
|
|
5
3
|
import * as Cause from "effect/Cause";
|
|
4
|
+
import * as Clock from "effect/Clock";
|
|
6
5
|
import * as Effect from "effect/Effect";
|
|
7
6
|
import * as Exit from "effect/Exit";
|
|
8
7
|
import * as Option from "effect/Option";
|
|
9
8
|
import * as Schema from "effect/Schema";
|
|
10
9
|
import { z } from "zod";
|
|
11
10
|
import { ask, catalog, declined, done, freshRequestId, } from "../internal/catalog.js";
|
|
12
|
-
import { Goal, GoalCommandInput, goalContract, GoalReason, Objective, } from "./contract.js";
|
|
11
|
+
import { EndedGoal, endedGoalLimit, Goal, GoalCommandInput, goalContract, GoalReason, Objective, } from "./contract.js";
|
|
12
|
+
/** 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 pauses until a person resumes it. Human pause/resume/clear remain human controls. Use plan.update_plan for multi-step work when available.";
|
|
13
14
|
/** Hashed prose and policy: every constant goal reads comes from here. */
|
|
14
15
|
const config = {
|
|
15
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.",
|
|
16
|
-
moduleDescription: "Session goal
|
|
17
|
-
|
|
17
|
+
moduleDescription: "Session goal tools: create_goal({objective}), get_goal({}), update_goal({status:'complete'|'blocked', reason}). " +
|
|
18
|
+
rules,
|
|
19
|
+
createDescription: "Create a Session goal, including autonomously. Fails while a goal is active or paused; a finished goal is history.",
|
|
18
20
|
getDescription: "Read the current goal. A requested terminal state remains provisional until successful settlement.",
|
|
19
|
-
updateDescription: "Request complete only after actual achievement, or blocked only after the same external impasse for three consecutive goal turns; settlement commits the request.",
|
|
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.",
|
|
20
22
|
promptLimit: 1_024,
|
|
21
23
|
stallTurns: 3,
|
|
22
|
-
stallReason: "Stalled: no progress for 3 turns
|
|
23
|
-
|
|
24
|
+
stallReason: "Stalled: no progress for 3 turns",
|
|
25
|
+
/** Why a goal paused when its tracked work did not complete. */
|
|
26
|
+
stoppedReasons: {
|
|
27
|
+
cancelled: "Stopped",
|
|
28
|
+
failed: "Goal work failed",
|
|
29
|
+
interrupted: "Goal work was interrupted",
|
|
30
|
+
outcome_unknown: "Goal work ended without a known outcome",
|
|
31
|
+
},
|
|
32
|
+
/** Why a live goal paused when another turn of its Session did not complete. */
|
|
33
|
+
otherStoppedReasons: {
|
|
34
|
+
failed: "The last turn failed",
|
|
35
|
+
interrupted: "The last turn was interrupted",
|
|
36
|
+
outcome_unknown: "The last turn ended without a known outcome",
|
|
37
|
+
},
|
|
38
|
+
noTerminalReason: "Goal work ended without a final reply",
|
|
39
|
+
idle: "Long-running objective? forage.describe({path:'goal'}) first.",
|
|
40
|
+
guide: "Session goal tools: goal.create_goal, goal.get_goal, goal.update_goal. " +
|
|
41
|
+
rules,
|
|
24
42
|
};
|
|
25
43
|
const promptEncoder = new TextEncoder();
|
|
26
44
|
const promptDecoder = new TextDecoder();
|
|
@@ -30,47 +48,63 @@ function boundedPrompt(text, byteLimit) {
|
|
|
30
48
|
const { written } = promptEncoder.encodeInto(text, bytes);
|
|
31
49
|
return promptDecoder.decode(bytes.subarray(0, written));
|
|
32
50
|
}
|
|
33
|
-
/** One Session's
|
|
51
|
+
/** One Session's current goal row; a finished goal leaves this collection. */
|
|
34
52
|
const GoalRow = Schema.Struct({
|
|
35
53
|
...Goal.fields,
|
|
36
54
|
sessionId: Schema.String,
|
|
37
55
|
/** A model-requested terminal, committed only by its run's success. */
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
pending: Schema.NullOr(Schema.Struct({
|
|
57
|
+
state: Schema.Literals(["complete", "blocked"]),
|
|
58
|
+
reason: GoalReason,
|
|
59
|
+
})),
|
|
40
60
|
stalledTurns: Schema.Int,
|
|
41
61
|
/** Request that set this revision; its retry replays. */
|
|
42
62
|
revisionRequestId: Schema.String,
|
|
43
63
|
/** Latest work whose settlement advances this goal. */
|
|
44
64
|
trackedWorkId: Schema.String,
|
|
45
65
|
});
|
|
66
|
+
/** One Session's finished goals, newest last, and how many were dropped. */
|
|
67
|
+
const EndingsRow = Schema.Struct({
|
|
68
|
+
sessionId: Schema.String,
|
|
69
|
+
ended: Schema.Array(EndedGoal).check(Schema.isMaxLength(endedGoalLimit)),
|
|
70
|
+
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
71
|
+
});
|
|
46
72
|
const collections = {
|
|
47
|
-
history: defineCollection(Schema.Struct({ sessionId: Schema.String, history: historySchema(Goal) }), { key: (row) => row.sessionId }),
|
|
48
73
|
goals: defineCollection(GoalRow, {
|
|
49
74
|
key: (goal) => goal.sessionId,
|
|
50
75
|
index: (goal) => goal.state,
|
|
51
76
|
}),
|
|
77
|
+
endings: defineCollection(EndingsRow, {
|
|
78
|
+
key: (endings) => endings.sessionId,
|
|
79
|
+
}),
|
|
52
80
|
};
|
|
53
81
|
function committed(row) {
|
|
54
82
|
return {
|
|
55
83
|
revision: row.revision,
|
|
56
84
|
objective: row.objective,
|
|
57
85
|
state: row.state,
|
|
58
|
-
|
|
86
|
+
startedAt: row.startedAt,
|
|
87
|
+
pauseCause: row.pauseCause,
|
|
59
88
|
};
|
|
60
89
|
}
|
|
61
90
|
function commitGoal(ctx, row) {
|
|
62
91
|
return Effect.gen(function* () {
|
|
63
|
-
const previous = Option.getOrUndefined(yield* ctx.state.goals.get(row.sessionId));
|
|
64
|
-
if ((row.state === "complete" || row.state === "blocked") &&
|
|
65
|
-
previous?.state !== row.state) {
|
|
66
|
-
const history = Option.getOrUndefined(yield* ctx.state.history.get(row.sessionId))
|
|
67
|
-
?.history ?? emptyHistory();
|
|
68
|
-
yield* ctx.state.history.put({
|
|
69
|
-
sessionId: row.sessionId,
|
|
70
|
-
history: appendHistory(history, committed(row), row.trackedWorkId || null, yield* Clock.currentTimeMillis),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
92
|
yield* ctx.state.goals.put(row);
|
|
93
|
+
if (row.state === "paused" &&
|
|
94
|
+
(row.pauseCause?.kind === "blocked" || row.pauseCause?.kind === "stalled"))
|
|
95
|
+
yield* ctx.attention
|
|
96
|
+
.set(row.sessionId, {
|
|
97
|
+
id: "paused-goal",
|
|
98
|
+
kind: `goal-${row.pauseCause.kind}`,
|
|
99
|
+
inbox: false,
|
|
100
|
+
summary: goalLine(committed(row)).slice(0, 200),
|
|
101
|
+
since: yield* Clock.currentTimeMillis,
|
|
102
|
+
})
|
|
103
|
+
.pipe(Effect.catchTag("ExtensionStateError", (error) => error.reason === "session_absent"
|
|
104
|
+
? Effect.void
|
|
105
|
+
: Effect.fail(error)));
|
|
106
|
+
else
|
|
107
|
+
yield* ctx.attention.clear(row.sessionId, "paused-goal");
|
|
74
108
|
});
|
|
75
109
|
}
|
|
76
110
|
function conflict(message) {
|
|
@@ -82,15 +116,54 @@ function requireGoal(ctx, sessionId) {
|
|
|
82
116
|
onSome: Effect.succeed,
|
|
83
117
|
})));
|
|
84
118
|
}
|
|
85
|
-
/** Pause
|
|
119
|
+
/** Pause a goal with the reason a person will see; provisional terminals drop. */
|
|
120
|
+
function paused(goal, kind, detail) {
|
|
121
|
+
return {
|
|
122
|
+
...goal,
|
|
123
|
+
state: "paused",
|
|
124
|
+
pauseCause: { kind, detail },
|
|
125
|
+
pending: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Finish the current goal: it leaves the current slot and joins the
|
|
130
|
+
* Session's bounded history, which the chat shows as one line.
|
|
131
|
+
*/
|
|
132
|
+
function finishGoal(ctx, goal, outcome, reason) {
|
|
133
|
+
return Effect.gen(function* () {
|
|
134
|
+
const endedAt = yield* Clock.currentTimeMillis;
|
|
135
|
+
const afterSequence = yield* ctx.sessionPosition(goal.sessionId);
|
|
136
|
+
const previous = Option.getOrUndefined(yield* ctx.state.endings.get(goal.sessionId));
|
|
137
|
+
const total = (previous?.total ?? 0) + 1;
|
|
138
|
+
const ending = {
|
|
139
|
+
sequence: total,
|
|
140
|
+
objective: goal.objective,
|
|
141
|
+
outcome,
|
|
142
|
+
reason,
|
|
143
|
+
endedAt,
|
|
144
|
+
afterSequence,
|
|
145
|
+
};
|
|
146
|
+
yield* ctx.state.endings.put({
|
|
147
|
+
sessionId: goal.sessionId,
|
|
148
|
+
ended: [...(previous?.ended ?? []), ending].slice(-endedGoalLimit),
|
|
149
|
+
total,
|
|
150
|
+
});
|
|
151
|
+
yield* ctx.state.goals.remove(goal.sessionId);
|
|
152
|
+
yield* ctx.attention.clear(goal.sessionId, "paused-goal");
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Pause or reactivate the current goal without touching admitted work. */
|
|
86
156
|
function changeActivity(ctx, sessionId, target) {
|
|
87
157
|
return Effect.gen(function* () {
|
|
88
158
|
const goal = yield* requireGoal(ctx, sessionId);
|
|
89
|
-
if (goal.state === "complete" || goal.state === "blocked")
|
|
90
|
-
return yield* Effect.fail(conflict("A terminal Session goal cannot change activity"));
|
|
91
159
|
const changed = target === "active"
|
|
92
|
-
? {
|
|
93
|
-
|
|
160
|
+
? {
|
|
161
|
+
...goal,
|
|
162
|
+
state: "active",
|
|
163
|
+
pauseCause: null,
|
|
164
|
+
stalledTurns: 0,
|
|
165
|
+
}
|
|
166
|
+
: paused(goal, "person", null);
|
|
94
167
|
yield* commitGoal(ctx, changed);
|
|
95
168
|
yield* ctx.observe(sessionId);
|
|
96
169
|
return changed;
|
|
@@ -112,16 +185,17 @@ function continueIfIdle(ctx, goal, requestId) {
|
|
|
112
185
|
})
|
|
113
186
|
.pipe(Effect.map((admitted) => admitted._tag === "Accepted"
|
|
114
187
|
? { ...goal, trackedWorkId: admitted.commandId }
|
|
115
|
-
: goal), Effect.catchTag("AdmissionError", (error) => Effect.succeed(
|
|
116
|
-
...goal,
|
|
117
|
-
state: "paused",
|
|
118
|
-
stopReason: error.message,
|
|
119
|
-
})));
|
|
188
|
+
: goal), Effect.catchTag("AdmissionError", (error) => Effect.succeed(paused(goal, "failed", error.message))));
|
|
120
189
|
}
|
|
121
190
|
/**
|
|
122
191
|
* Advance one Session's goal from one settled run. Any successful run in
|
|
123
|
-
* the Session becomes the tracked progress
|
|
124
|
-
*
|
|
192
|
+
* the Session becomes the tracked progress. Tracked work that did not
|
|
193
|
+
* complete (Esc, failure, or a restart whose resumed turn could not finish),
|
|
194
|
+
* or any other turn of the Session that failed or was stopped while the goal
|
|
195
|
+
* was live,
|
|
196
|
+
* pauses the goal until a person resumes it; a turn a restart resumed
|
|
197
|
+
* settles here like any other, so the goal keeps going. An achieved goal
|
|
198
|
+
* finishes and leaves the current slot.
|
|
125
199
|
*/
|
|
126
200
|
function settle(ctx, run) {
|
|
127
201
|
return Effect.gen(function* () {
|
|
@@ -129,30 +203,26 @@ function settle(ctx, run) {
|
|
|
129
203
|
if (Option.isNone(found))
|
|
130
204
|
return;
|
|
131
205
|
const goal = found.value;
|
|
132
|
-
if (goal.state !== "active" && goal.state !== "paused")
|
|
133
|
-
return;
|
|
134
206
|
if (run.outcome !== "completed") {
|
|
135
|
-
|
|
207
|
+
const tracked = run.workId === goal.trackedWorkId;
|
|
208
|
+
if (!tracked && goal.state !== "active")
|
|
136
209
|
return;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
210
|
+
const cancelled = run.outcome === "cancelled";
|
|
211
|
+
// Esc stops a live goal even on another turn. Restart-resumed work never
|
|
212
|
+
// settles here; other failures pause the goal with their own display line.
|
|
213
|
+
const detail = tracked || cancelled
|
|
214
|
+
? ctx.config.stoppedReasons[run.outcome]
|
|
215
|
+
: ctx.config.otherStoppedReasons[run.outcome];
|
|
216
|
+
yield* commitGoal(ctx, paused(goal, cancelled ? "cancelled" : "failed", detail));
|
|
143
217
|
return yield* ctx.observe(run.sessionId);
|
|
144
218
|
}
|
|
145
219
|
if (run.terminal !== "completed") {
|
|
146
|
-
yield* commitGoal(ctx,
|
|
147
|
-
...goal,
|
|
148
|
-
stopReason: "Completed goal work has no exact terminal",
|
|
149
|
-
});
|
|
220
|
+
yield* commitGoal(ctx, paused(goal, "no-terminal", ctx.config.noTerminalReason));
|
|
150
221
|
return yield* ctx.observe(run.sessionId);
|
|
151
222
|
}
|
|
152
223
|
const tracked = {
|
|
153
224
|
...goal,
|
|
154
225
|
trackedWorkId: run.workId,
|
|
155
|
-
stopReason: null,
|
|
156
226
|
stalledTurns: run.origin?.extensionId === goalContract.id &&
|
|
157
227
|
run.origin.correlation === goal.revision
|
|
158
228
|
? run.toolCalls === 0
|
|
@@ -160,21 +230,14 @@ function settle(ctx, run) {
|
|
|
160
230
|
: 0
|
|
161
231
|
: goal.stalledTurns,
|
|
162
232
|
};
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
pendingState: null,
|
|
169
|
-
pendingReason: null,
|
|
170
|
-
});
|
|
233
|
+
const pending = goal.trackedWorkId === run.workId ? goal.pending : null;
|
|
234
|
+
if (pending?.state === "complete")
|
|
235
|
+
yield* finishGoal(ctx, tracked, "achieved", pending.reason);
|
|
236
|
+
else if (pending?.state === "blocked")
|
|
237
|
+
yield* commitGoal(ctx, paused(tracked, "blocked", pending.reason));
|
|
171
238
|
else if (goal.state === "active" &&
|
|
172
239
|
tracked.stalledTurns >= ctx.config.stallTurns)
|
|
173
|
-
yield* commitGoal(ctx,
|
|
174
|
-
...tracked,
|
|
175
|
-
state: "paused",
|
|
176
|
-
stopReason: ctx.config.stallReason,
|
|
177
|
-
});
|
|
240
|
+
yield* commitGoal(ctx, paused(tracked, "stalled", ctx.config.stallReason));
|
|
178
241
|
else if (goal.state === "active")
|
|
179
242
|
yield* commitGoal(ctx, yield* continueIfIdle(ctx, tracked, `goal-continue:${run.workId}`));
|
|
180
243
|
else
|
|
@@ -215,22 +278,19 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
215
278
|
}),
|
|
216
279
|
execute: ({ objective }) => toolCall(ctx, Effect.gen(function* () {
|
|
217
280
|
const decoded = yield* Schema.decodeUnknownEffect(Objective)(objective).pipe(Effect.mapError(() => conflict("Goal objective is invalid")));
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return yield* Effect.fail(conflict(current.value.state === "blocked"
|
|
221
|
-
? "A blocked Session goal cannot be replaced"
|
|
222
|
-
: "A nonterminal Session goal already exists"));
|
|
281
|
+
if (Option.isSome(yield* ctx.state.goals.get(sessionId)))
|
|
282
|
+
return yield* Effect.fail(conflict("A Session goal is already active or paused; only a person replaces it"));
|
|
223
283
|
yield* commitGoal(ctx, {
|
|
224
284
|
sessionId,
|
|
225
285
|
objective: decoded,
|
|
226
286
|
state: "active",
|
|
227
|
-
|
|
228
|
-
|
|
287
|
+
startedAt: yield* Clock.currentTimeMillis,
|
|
288
|
+
pending: null,
|
|
229
289
|
stalledTurns: 0,
|
|
230
290
|
revision: `run:${workId}`,
|
|
231
291
|
revisionRequestId: `run:${workId}`,
|
|
232
292
|
trackedWorkId: workId,
|
|
233
|
-
|
|
293
|
+
pauseCause: null,
|
|
234
294
|
});
|
|
235
295
|
yield* ctx.observe(sessionId);
|
|
236
296
|
return { objective: decoded, state: "active" };
|
|
@@ -244,7 +304,7 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
244
304
|
z.strictObject({
|
|
245
305
|
objective: toolObjective,
|
|
246
306
|
state: toolState,
|
|
247
|
-
|
|
307
|
+
reason: z.string().nullable(),
|
|
248
308
|
}),
|
|
249
309
|
z.null(),
|
|
250
310
|
]),
|
|
@@ -252,8 +312,10 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
252
312
|
? null
|
|
253
313
|
: {
|
|
254
314
|
objective: found.value.objective,
|
|
255
|
-
state: found.value.
|
|
256
|
-
|
|
315
|
+
state: found.value.pending?.state ?? found.value.state,
|
|
316
|
+
reason: found.value.pending?.reason ??
|
|
317
|
+
found.value.pauseCause?.detail ??
|
|
318
|
+
null,
|
|
257
319
|
}))),
|
|
258
320
|
}),
|
|
259
321
|
update_goal: Mycelium.operation({
|
|
@@ -277,8 +339,7 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
277
339
|
return yield* Effect.fail(conflict("Session goal work guard is stale"));
|
|
278
340
|
yield* commitGoal(ctx, {
|
|
279
341
|
...found.value,
|
|
280
|
-
|
|
281
|
-
pendingReason: decoded,
|
|
342
|
+
pending: { state: status, reason: decoded },
|
|
282
343
|
});
|
|
283
344
|
return {
|
|
284
345
|
objective: found.value.objective,
|
|
@@ -332,17 +393,20 @@ function setGoal(ctx, sessionId, input) {
|
|
|
332
393
|
return yield* Effect.fail(conflict("Session goal changed after this request was accepted"));
|
|
333
394
|
return { goal: committed(current), accepted };
|
|
334
395
|
}
|
|
396
|
+
// A replaced goal ends as cleared: history, never current.
|
|
397
|
+
if (current !== undefined && !retried)
|
|
398
|
+
yield* finishGoal(ctx, current, "cleared", null);
|
|
335
399
|
const row = {
|
|
336
400
|
sessionId,
|
|
337
401
|
objective: input.objective,
|
|
338
402
|
state: "active",
|
|
339
|
-
|
|
340
|
-
|
|
403
|
+
startedAt: yield* Clock.currentTimeMillis,
|
|
404
|
+
pending: null,
|
|
341
405
|
stalledTurns: 0,
|
|
342
406
|
revision: `set:${input.requestId}`,
|
|
343
407
|
revisionRequestId: input.requestId,
|
|
344
408
|
trackedWorkId: admitted.commandId,
|
|
345
|
-
|
|
409
|
+
pauseCause: null,
|
|
346
410
|
};
|
|
347
411
|
yield* commitGoal(ctx, row);
|
|
348
412
|
yield* ctx.observe(sessionId);
|
|
@@ -350,20 +414,27 @@ function setGoal(ctx, sessionId, input) {
|
|
|
350
414
|
});
|
|
351
415
|
}
|
|
352
416
|
function goalLine(goal) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
417
|
+
if (goal.state === "active")
|
|
418
|
+
return `Pursuing goal: ${goal.objective}`;
|
|
419
|
+
return goal.pauseCause?.detail == null
|
|
420
|
+
? `Goal paused: ${goal.objective}`
|
|
421
|
+
: `Goal paused: ${goal.objective} (${goal.pauseCause.detail})`;
|
|
357
422
|
}
|
|
358
423
|
const objectivePrompt = {
|
|
359
424
|
type: "text",
|
|
360
425
|
message: "What should this Session work toward?",
|
|
361
426
|
placeholder: "Describe the finished result",
|
|
362
427
|
};
|
|
428
|
+
/** Clear the current goal into history; returns whether one was current. */
|
|
363
429
|
function clearGoal(ctx, sessionId) {
|
|
364
|
-
return
|
|
365
|
-
.
|
|
366
|
-
|
|
430
|
+
return Effect.gen(function* () {
|
|
431
|
+
const current = yield* ctx.state.goals.get(sessionId);
|
|
432
|
+
if (Option.isNone(current))
|
|
433
|
+
return false;
|
|
434
|
+
yield* finishGoal(ctx, current.value, "cleared", null);
|
|
435
|
+
yield* ctx.observe(sessionId);
|
|
436
|
+
return true;
|
|
437
|
+
});
|
|
367
438
|
}
|
|
368
439
|
/**
|
|
369
440
|
* The human `/goal` conversation. Every step is decided here from the
|
|
@@ -376,30 +447,24 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
376
447
|
if (current === undefined)
|
|
377
448
|
return ask(objectivePrompt, { action: "set", requestId: yield* freshRequestId }, "objective");
|
|
378
449
|
const goal = committed(current);
|
|
379
|
-
const options = goal.state === "active"
|
|
380
|
-
? [{ value: "pause", label: "Pause goal" }]
|
|
381
|
-
: goal.state === "paused"
|
|
382
|
-
? [{ value: "resume", label: "Resume goal" }]
|
|
383
|
-
: [];
|
|
384
450
|
return ask({
|
|
385
451
|
type: "choice",
|
|
386
452
|
message: goalLine(goal),
|
|
387
453
|
options: [
|
|
388
|
-
|
|
454
|
+
goal.state === "active"
|
|
455
|
+
? { value: "pause", label: "Pause goal" }
|
|
456
|
+
: { value: "resume", label: "Resume goal" },
|
|
389
457
|
{ value: "clear", label: "Clear goal" },
|
|
390
|
-
{
|
|
391
|
-
value: "replace",
|
|
392
|
-
label: goal.state === "complete" || goal.state === "blocked"
|
|
393
|
-
? "Set a new goal"
|
|
394
|
-
: "Replace goal",
|
|
395
|
-
},
|
|
458
|
+
{ value: "replace", label: "Replace goal" },
|
|
396
459
|
],
|
|
397
460
|
}, {}, "action");
|
|
398
461
|
}
|
|
399
462
|
switch (input.action) {
|
|
400
463
|
case "clear":
|
|
401
|
-
|
|
402
|
-
return
|
|
464
|
+
// The chat's ending line confirms a clear; only an empty one says so.
|
|
465
|
+
return (yield* clearGoal(ctx, sessionId))
|
|
466
|
+
? { type: "done" }
|
|
467
|
+
: done({ type: "info", message: "No goal to clear." });
|
|
403
468
|
case "pause":
|
|
404
469
|
yield* pauseGoal(ctx, sessionId);
|
|
405
470
|
return { type: "done" };
|
|
@@ -415,13 +480,14 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
415
480
|
}, "objective");
|
|
416
481
|
case "set": {
|
|
417
482
|
const retried = current?.revisionRequestId === input.requestId;
|
|
418
|
-
|
|
483
|
+
// Only a live or paused goal is current, so only it asks first.
|
|
484
|
+
const replacing = current !== undefined && !retried;
|
|
419
485
|
if (replacing && input.confirmed === false)
|
|
420
486
|
return declined({ type: "info", message: "Kept the current goal." });
|
|
421
487
|
if (replacing && input.confirmed !== true)
|
|
422
488
|
return ask({
|
|
423
489
|
type: "confirmation",
|
|
424
|
-
message: `Replace the current goal?\nCurrent: ${current.objective}\nNew: ${input.objective}`,
|
|
490
|
+
message: `Replace the ${current.state === "active" ? "live" : "paused"} goal?\nCurrent: ${current.objective}\nNew: ${input.objective}`,
|
|
425
491
|
confirmLabel: "Replace and start",
|
|
426
492
|
cancelLabel: "Keep current goal",
|
|
427
493
|
}, {
|
|
@@ -486,18 +552,25 @@ export const goalCatalog = catalog([
|
|
|
486
552
|
/** Botanical's Session goal, driven only by settled runs. */
|
|
487
553
|
export const goalExtension = defineExtension({
|
|
488
554
|
id: goalContract.id,
|
|
489
|
-
version: "
|
|
555
|
+
version: "7.0.0",
|
|
490
556
|
})
|
|
491
557
|
.config(config)
|
|
492
|
-
|
|
558
|
+
// A command anchor cannot be converted into a historical Session position.
|
|
559
|
+
// Old preview state must be republished separately, never guessed here.
|
|
560
|
+
.state({ version: 7, migrations: {}, collections })
|
|
493
561
|
.define({
|
|
562
|
+
attention: true,
|
|
494
563
|
prompt: (ctx, { sessionId, origin }) => Effect.gen(function* () {
|
|
495
564
|
const goal = Option.getOrNull(yield* ctx.state.goals.get(sessionId));
|
|
496
|
-
const
|
|
565
|
+
const text = goal === null
|
|
566
|
+
? ctx.config.idle
|
|
567
|
+
: (goal.state === "active" ? ctx.config.guide : ctx.config.idle) +
|
|
568
|
+
"\n" +
|
|
569
|
+
goalLine(committed(goal));
|
|
497
570
|
return [
|
|
498
571
|
{
|
|
499
572
|
name: "goal",
|
|
500
|
-
text: boundedPrompt(
|
|
573
|
+
text: boundedPrompt(text, ctx.config.promptLimit),
|
|
501
574
|
},
|
|
502
575
|
...(goal?.state === "active" &&
|
|
503
576
|
origin?.extensionId === goalContract.id &&
|
|
@@ -516,13 +589,15 @@ export const goalExtension = defineExtension({
|
|
|
516
589
|
schema: goalContract.projection.schema,
|
|
517
590
|
read: (ctx, sessionId) => {
|
|
518
591
|
const goal = Option.getOrUndefined(ctx.state.goals.peek(sessionId));
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
592
|
+
const endings = Option.getOrUndefined(ctx.state.endings.peek(sessionId));
|
|
593
|
+
if (goal === undefined && endings === undefined)
|
|
594
|
+
return Option.none();
|
|
595
|
+
const ended = endings?.ended ?? [];
|
|
596
|
+
return Option.some({
|
|
597
|
+
goal: goal === undefined ? null : committed(goal),
|
|
598
|
+
ended,
|
|
599
|
+
omitted: (endings?.total ?? 0) - ended.length,
|
|
600
|
+
});
|
|
526
601
|
},
|
|
527
602
|
},
|
|
528
603
|
hooks: {
|
|
@@ -535,7 +610,7 @@ export const goalExtension = defineExtension({
|
|
|
535
610
|
goal: ({ sessionId, input }) => humanGoal(ctx, sessionId, input),
|
|
536
611
|
pause: ({ sessionId }) => pauseGoal(ctx, sessionId),
|
|
537
612
|
resume: ({ sessionId }) => resumeGoal(ctx, sessionId),
|
|
538
|
-
clear: ({ sessionId }) => clearGoal(ctx, sessionId),
|
|
613
|
+
clear: ({ sessionId }) => clearGoal(ctx, sessionId).pipe(Effect.as({})),
|
|
539
614
|
set: ({ sessionId, input }) => setGoal(ctx, sessionId, input),
|
|
540
615
|
}),
|
|
541
616
|
},
|
package/dist/home/contract.d.ts
CHANGED
|
@@ -1,32 +1,50 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
2
|
/** The Agent's recorded choice survives the chosen attachment being detached. */
|
|
3
3
|
export declare const Home: Schema.Struct<{
|
|
4
|
+
/** Chosen Computer attachment, or null when no Home is selected. */
|
|
4
5
|
readonly attachmentId: Schema.NullOr<Schema.String>;
|
|
5
6
|
}>;
|
|
7
|
+
/** Persisted Home selection for one Agent. */
|
|
6
8
|
export type Home = Schema.Schema.Type<typeof Home>;
|
|
7
9
|
/** Human selection changes Agent state; no model operation exposes this command. */
|
|
8
10
|
export declare const homeContract: {
|
|
11
|
+
/** Stable extension identity. */
|
|
9
12
|
id: string & import("effect/Brand").Brand<"ExtensionId">;
|
|
13
|
+
/** Human selection and initial seeding operations. */
|
|
10
14
|
commands: {
|
|
15
|
+
/** Open Home controls or select an attachment by name. */
|
|
11
16
|
home: {
|
|
17
|
+
/** Human controls are opened from a Session. */
|
|
12
18
|
scope: "session";
|
|
19
|
+
/** Decode an optional attachment name. */
|
|
13
20
|
input: Schema.Struct<{
|
|
21
|
+
/** Attachment name selected by the person. */
|
|
14
22
|
readonly name: Schema.optionalKey<Schema.String>;
|
|
15
23
|
}>;
|
|
24
|
+
/** Return the server-owned conversation step. */
|
|
16
25
|
output: Schema.declare<import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1, import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1>;
|
|
17
26
|
};
|
|
27
|
+
/** Seed the Agent's initial recorded Home choice. */
|
|
18
28
|
seed: {
|
|
29
|
+
/** Home selection belongs to the Agent. */
|
|
19
30
|
scope: "agent";
|
|
31
|
+
/** Decode the initial selection. */
|
|
20
32
|
input: Schema.Struct<{
|
|
33
|
+
/** Chosen Computer attachment, or null when no Home is selected. */
|
|
21
34
|
readonly attachmentId: Schema.NullOr<Schema.String>;
|
|
22
35
|
}>;
|
|
36
|
+
/** Report whether seeding changed the recorded choice. */
|
|
23
37
|
output: Schema.Struct<{
|
|
38
|
+
/** Whether the initial choice was seeded. */
|
|
24
39
|
readonly seeded: Schema.Boolean;
|
|
25
40
|
}>;
|
|
26
41
|
};
|
|
27
42
|
};
|
|
43
|
+
/** Recorded selection visible to clients, or no seeded state. */
|
|
28
44
|
projection: {
|
|
45
|
+
/** Decoder for the optional Home state. */
|
|
29
46
|
schema: Schema.NullOr<Schema.Struct<{
|
|
47
|
+
/** Chosen Computer attachment, or null when no Home is selected. */
|
|
30
48
|
readonly attachmentId: Schema.NullOr<Schema.String>;
|
|
31
49
|
}>>;
|
|
32
50
|
};
|
package/dist/home/contract.js
CHANGED
|
@@ -4,22 +4,43 @@ import { CatalogStep } from "../internal/catalog.js";
|
|
|
4
4
|
const attachmentId = Schema.String.pipe(Schema.check(Schema.isMinLength(1)));
|
|
5
5
|
/** The Agent's recorded choice survives the chosen attachment being detached. */
|
|
6
6
|
export const Home = Schema.Struct({
|
|
7
|
+
/** Chosen Computer attachment, or null when no Home is selected. */
|
|
7
8
|
attachmentId: Schema.NullOr(attachmentId),
|
|
8
9
|
});
|
|
9
10
|
/** Human selection changes Agent state; no model operation exposes this command. */
|
|
10
11
|
export const homeContract = {
|
|
12
|
+
/** Stable extension identity. */
|
|
11
13
|
id: ExtensionId.make("botanical-home"),
|
|
14
|
+
/** Human selection and initial seeding operations. */
|
|
12
15
|
commands: {
|
|
16
|
+
/** Open Home controls or select an attachment by name. */
|
|
13
17
|
home: {
|
|
18
|
+
/** Human controls are opened from a Session. */
|
|
14
19
|
scope: "session",
|
|
15
|
-
|
|
20
|
+
/** Decode an optional attachment name. */
|
|
21
|
+
input: Schema.Struct({
|
|
22
|
+
/** Attachment name selected by the person. */
|
|
23
|
+
name: Schema.optionalKey(Schema.String),
|
|
24
|
+
}),
|
|
25
|
+
/** Return the server-owned conversation step. */
|
|
16
26
|
output: CatalogStep,
|
|
17
27
|
},
|
|
28
|
+
/** Seed the Agent's initial recorded Home choice. */
|
|
18
29
|
seed: {
|
|
30
|
+
/** Home selection belongs to the Agent. */
|
|
19
31
|
scope: "agent",
|
|
32
|
+
/** Decode the initial selection. */
|
|
20
33
|
input: Home,
|
|
21
|
-
|
|
34
|
+
/** Report whether seeding changed the recorded choice. */
|
|
35
|
+
output: Schema.Struct({
|
|
36
|
+
/** Whether the initial choice was seeded. */
|
|
37
|
+
seeded: Schema.Boolean,
|
|
38
|
+
}),
|
|
22
39
|
},
|
|
23
40
|
},
|
|
24
|
-
|
|
41
|
+
/** Recorded selection visible to clients, or no seeded state. */
|
|
42
|
+
projection: {
|
|
43
|
+
/** Decoder for the optional Home state. */
|
|
44
|
+
schema: Schema.NullOr(Home),
|
|
45
|
+
},
|
|
25
46
|
};
|
package/dist/home/host.d.ts
CHANGED
|
@@ -2,8 +2,11 @@ import { type FilesPort } from "@fungi.computer/knapsack";
|
|
|
2
2
|
import { type ExtensionGrantV0, type RosteredExtensionV0 } from "@fungi.computer/shiitake/extensions";
|
|
3
3
|
/** Already-scoped authority supplied by the host, never constructed by Home. */
|
|
4
4
|
export type HomeComputer = Readonly<{
|
|
5
|
+
/** Attachment name offered to the person choosing Home. */
|
|
5
6
|
name: string;
|
|
7
|
+
/** Stable identity stored by the Home selection. */
|
|
6
8
|
attachmentId: string;
|
|
9
|
+
/** Already-scoped Files authority captured for this attachment. */
|
|
7
10
|
port: FilesPort;
|
|
8
11
|
}>;
|
|
9
12
|
/** Hosts admit Home explicitly; no other extension receives the Files slot. */
|
package/dist/home/host.js
CHANGED
|
@@ -13,7 +13,7 @@ export const homeGrant = {
|
|
|
13
13
|
};
|
|
14
14
|
const config = {
|
|
15
15
|
promptLimit: 4_000,
|
|
16
|
-
guide: "Home supplies read, list, glob and grep over your default Computer. Bare paths and computer://home/ use Home; computer://<name>/ uses that captured attachment. apply_patch, when offered,
|
|
16
|
+
guide: "Home supplies read, list, glob and grep over your default Computer. Bare paths and computer://home/ use Home; computer://<name>/ uses that captured attachment. apply_patch, when offered, also works on Home. Only a person changes Home with /home <name>; ask them when no Home is available.",
|
|
17
17
|
};
|
|
18
18
|
const collections = {
|
|
19
19
|
choice: defineCollection(Home, { key: () => "default" }),
|
|
@@ -79,7 +79,6 @@ export function homeExtension(options) {
|
|
|
79
79
|
return null;
|
|
80
80
|
return Knapsack.files({
|
|
81
81
|
port: homeFilesRouting(new Map(computers.map(({ name, port }) => [name, port])), selected.port),
|
|
82
|
-
pathInstructions: "Bare paths and computer://home/ select Home. computer://<name>/ selects another captured attachment. apply_patch uses Home only.",
|
|
83
82
|
}).tools;
|
|
84
83
|
},
|
|
85
84
|
prompt: (_ctx, run) => [
|