@fungi.computer/shiitake-extensions 0.0.2 → 0.1.1
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 +206 -124
- 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 +129 -84
- package/dist/loop/contract.js +79 -9
- package/dist/loop/host.d.ts +1 -0
- package/dist/loop/host.js +89 -48
- 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 +161 -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
|
|
2
|
-
import { appendHistory, emptyHistory, historySchema, } from "../internal/history.js";
|
|
3
|
-
import { Mycelium } from "@fungi.computer/mycelium";
|
|
1
|
+
import { CapabilityFailed, 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
|
|
@@ -182,17 +245,24 @@ function settle(ctx, run) {
|
|
|
182
245
|
yield* ctx.observe(run.sessionId);
|
|
183
246
|
});
|
|
184
247
|
}
|
|
185
|
-
/** Run one tool transaction and
|
|
186
|
-
function toolCall(ctx, body) {
|
|
248
|
+
/** Run one tool transaction and preserve its owner-admitted diagnostic. */
|
|
249
|
+
function toolCall(ctx, operation, body) {
|
|
187
250
|
return Effect.runPromiseExit(ctx.transact(body)).then((exit) => {
|
|
188
251
|
if (Exit.isSuccess(exit))
|
|
189
252
|
return exit.value;
|
|
190
253
|
const failure = Cause.squash(exit.cause);
|
|
191
|
-
return Promise.reject(new
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
: "
|
|
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
|
+
}));
|
|
196
266
|
});
|
|
197
267
|
}
|
|
198
268
|
// Mycelium's maintained operation interface takes Zod for model-tool I/O.
|
|
@@ -213,24 +283,21 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
213
283
|
objective: toolObjective,
|
|
214
284
|
state: z.literal("active"),
|
|
215
285
|
}),
|
|
216
|
-
execute: ({ objective }) => toolCall(ctx, Effect.gen(function* () {
|
|
286
|
+
execute: ({ objective }) => toolCall(ctx, "create_goal", Effect.gen(function* () {
|
|
217
287
|
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"));
|
|
288
|
+
if (Option.isSome(yield* ctx.state.goals.get(sessionId)))
|
|
289
|
+
return yield* Effect.fail(conflict("A Session goal is already active or paused; only a person replaces it"));
|
|
223
290
|
yield* commitGoal(ctx, {
|
|
224
291
|
sessionId,
|
|
225
292
|
objective: decoded,
|
|
226
293
|
state: "active",
|
|
227
|
-
|
|
228
|
-
|
|
294
|
+
startedAt: yield* Clock.currentTimeMillis,
|
|
295
|
+
pending: null,
|
|
229
296
|
stalledTurns: 0,
|
|
230
297
|
revision: `run:${workId}`,
|
|
231
298
|
revisionRequestId: `run:${workId}`,
|
|
232
299
|
trackedWorkId: workId,
|
|
233
|
-
|
|
300
|
+
pauseCause: null,
|
|
234
301
|
});
|
|
235
302
|
yield* ctx.observe(sessionId);
|
|
236
303
|
return { objective: decoded, state: "active" };
|
|
@@ -244,16 +311,18 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
244
311
|
z.strictObject({
|
|
245
312
|
objective: toolObjective,
|
|
246
313
|
state: toolState,
|
|
247
|
-
|
|
314
|
+
reason: z.string().nullable(),
|
|
248
315
|
}),
|
|
249
316
|
z.null(),
|
|
250
317
|
]),
|
|
251
|
-
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)
|
|
252
319
|
? null
|
|
253
320
|
: {
|
|
254
321
|
objective: found.value.objective,
|
|
255
|
-
state: found.value.
|
|
256
|
-
|
|
322
|
+
state: found.value.pending?.state ?? found.value.state,
|
|
323
|
+
reason: found.value.pending?.reason ??
|
|
324
|
+
found.value.pauseCause?.detail ??
|
|
325
|
+
null,
|
|
257
326
|
}))),
|
|
258
327
|
}),
|
|
259
328
|
update_goal: Mycelium.operation({
|
|
@@ -268,7 +337,7 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
268
337
|
state: z.enum(["complete", "blocked"]),
|
|
269
338
|
reason: z.string(),
|
|
270
339
|
}),
|
|
271
|
-
execute: ({ status, reason }) => toolCall(ctx, Effect.gen(function* () {
|
|
340
|
+
execute: ({ status, reason }) => toolCall(ctx, "update_goal", Effect.gen(function* () {
|
|
272
341
|
const decoded = yield* Schema.decodeUnknownEffect(GoalReason)(reason).pipe(Effect.mapError(() => conflict("Goal reason is invalid")));
|
|
273
342
|
const found = yield* ctx.state.goals.get(sessionId);
|
|
274
343
|
if (Option.isNone(found) ||
|
|
@@ -277,8 +346,7 @@ function goalModule(ctx, sessionId, workId) {
|
|
|
277
346
|
return yield* Effect.fail(conflict("Session goal work guard is stale"));
|
|
278
347
|
yield* commitGoal(ctx, {
|
|
279
348
|
...found.value,
|
|
280
|
-
|
|
281
|
-
pendingReason: decoded,
|
|
349
|
+
pending: { state: status, reason: decoded },
|
|
282
350
|
});
|
|
283
351
|
return {
|
|
284
352
|
objective: found.value.objective,
|
|
@@ -332,17 +400,20 @@ function setGoal(ctx, sessionId, input) {
|
|
|
332
400
|
return yield* Effect.fail(conflict("Session goal changed after this request was accepted"));
|
|
333
401
|
return { goal: committed(current), accepted };
|
|
334
402
|
}
|
|
403
|
+
// A replaced goal ends as cleared: history, never current.
|
|
404
|
+
if (current !== undefined && !retried)
|
|
405
|
+
yield* finishGoal(ctx, current, "cleared", null);
|
|
335
406
|
const row = {
|
|
336
407
|
sessionId,
|
|
337
408
|
objective: input.objective,
|
|
338
409
|
state: "active",
|
|
339
|
-
|
|
340
|
-
|
|
410
|
+
startedAt: yield* Clock.currentTimeMillis,
|
|
411
|
+
pending: null,
|
|
341
412
|
stalledTurns: 0,
|
|
342
413
|
revision: `set:${input.requestId}`,
|
|
343
414
|
revisionRequestId: input.requestId,
|
|
344
415
|
trackedWorkId: admitted.commandId,
|
|
345
|
-
|
|
416
|
+
pauseCause: null,
|
|
346
417
|
};
|
|
347
418
|
yield* commitGoal(ctx, row);
|
|
348
419
|
yield* ctx.observe(sessionId);
|
|
@@ -350,20 +421,27 @@ function setGoal(ctx, sessionId, input) {
|
|
|
350
421
|
});
|
|
351
422
|
}
|
|
352
423
|
function goalLine(goal) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
424
|
+
if (goal.state === "active")
|
|
425
|
+
return `Pursuing goal: ${goal.objective}`;
|
|
426
|
+
return goal.pauseCause?.detail == null
|
|
427
|
+
? `Goal paused: ${goal.objective}`
|
|
428
|
+
: `Goal paused: ${goal.objective} (${goal.pauseCause.detail})`;
|
|
357
429
|
}
|
|
358
430
|
const objectivePrompt = {
|
|
359
431
|
type: "text",
|
|
360
432
|
message: "What should this Session work toward?",
|
|
361
433
|
placeholder: "Describe the finished result",
|
|
362
434
|
};
|
|
435
|
+
/** Clear the current goal into history; returns whether one was current. */
|
|
363
436
|
function clearGoal(ctx, sessionId) {
|
|
364
|
-
return
|
|
365
|
-
.
|
|
366
|
-
|
|
437
|
+
return Effect.gen(function* () {
|
|
438
|
+
const current = yield* ctx.state.goals.get(sessionId);
|
|
439
|
+
if (Option.isNone(current))
|
|
440
|
+
return false;
|
|
441
|
+
yield* finishGoal(ctx, current.value, "cleared", null);
|
|
442
|
+
yield* ctx.observe(sessionId);
|
|
443
|
+
return true;
|
|
444
|
+
});
|
|
367
445
|
}
|
|
368
446
|
/**
|
|
369
447
|
* The human `/goal` conversation. Every step is decided here from the
|
|
@@ -376,30 +454,24 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
376
454
|
if (current === undefined)
|
|
377
455
|
return ask(objectivePrompt, { action: "set", requestId: yield* freshRequestId }, "objective");
|
|
378
456
|
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
457
|
return ask({
|
|
385
458
|
type: "choice",
|
|
386
459
|
message: goalLine(goal),
|
|
387
460
|
options: [
|
|
388
|
-
|
|
461
|
+
goal.state === "active"
|
|
462
|
+
? { value: "pause", label: "Pause goal" }
|
|
463
|
+
: { value: "resume", label: "Resume goal" },
|
|
389
464
|
{ 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
|
-
},
|
|
465
|
+
{ value: "replace", label: "Replace goal" },
|
|
396
466
|
],
|
|
397
467
|
}, {}, "action");
|
|
398
468
|
}
|
|
399
469
|
switch (input.action) {
|
|
400
470
|
case "clear":
|
|
401
|
-
|
|
402
|
-
return
|
|
471
|
+
// The chat's ending line confirms a clear; only an empty one says so.
|
|
472
|
+
return (yield* clearGoal(ctx, sessionId))
|
|
473
|
+
? { type: "done" }
|
|
474
|
+
: done({ type: "info", message: "No goal to clear." });
|
|
403
475
|
case "pause":
|
|
404
476
|
yield* pauseGoal(ctx, sessionId);
|
|
405
477
|
return { type: "done" };
|
|
@@ -415,13 +487,14 @@ function humanGoal(ctx, sessionId, input) {
|
|
|
415
487
|
}, "objective");
|
|
416
488
|
case "set": {
|
|
417
489
|
const retried = current?.revisionRequestId === input.requestId;
|
|
418
|
-
|
|
490
|
+
// Only a live or paused goal is current, so only it asks first.
|
|
491
|
+
const replacing = current !== undefined && !retried;
|
|
419
492
|
if (replacing && input.confirmed === false)
|
|
420
493
|
return declined({ type: "info", message: "Kept the current goal." });
|
|
421
494
|
if (replacing && input.confirmed !== true)
|
|
422
495
|
return ask({
|
|
423
496
|
type: "confirmation",
|
|
424
|
-
message: `Replace the current goal?\nCurrent: ${current.objective}\nNew: ${input.objective}`,
|
|
497
|
+
message: `Replace the ${current.state === "active" ? "live" : "paused"} goal?\nCurrent: ${current.objective}\nNew: ${input.objective}`,
|
|
425
498
|
confirmLabel: "Replace and start",
|
|
426
499
|
cancelLabel: "Keep current goal",
|
|
427
500
|
}, {
|
|
@@ -486,18 +559,25 @@ export const goalCatalog = catalog([
|
|
|
486
559
|
/** Botanical's Session goal, driven only by settled runs. */
|
|
487
560
|
export const goalExtension = defineExtension({
|
|
488
561
|
id: goalContract.id,
|
|
489
|
-
version: "
|
|
562
|
+
version: "7.0.0",
|
|
490
563
|
})
|
|
491
564
|
.config(config)
|
|
492
|
-
|
|
565
|
+
// A command anchor cannot be converted into a historical Session position.
|
|
566
|
+
// Old preview state must be republished separately, never guessed here.
|
|
567
|
+
.state({ version: 7, migrations: {}, collections })
|
|
493
568
|
.define({
|
|
569
|
+
attention: true,
|
|
494
570
|
prompt: (ctx, { sessionId, origin }) => Effect.gen(function* () {
|
|
495
571
|
const goal = Option.getOrNull(yield* ctx.state.goals.get(sessionId));
|
|
496
|
-
const
|
|
572
|
+
const text = goal === null
|
|
573
|
+
? ctx.config.idle
|
|
574
|
+
: (goal.state === "active" ? ctx.config.guide : ctx.config.idle) +
|
|
575
|
+
"\n" +
|
|
576
|
+
goalLine(committed(goal));
|
|
497
577
|
return [
|
|
498
578
|
{
|
|
499
579
|
name: "goal",
|
|
500
|
-
text: boundedPrompt(
|
|
580
|
+
text: boundedPrompt(text, ctx.config.promptLimit),
|
|
501
581
|
},
|
|
502
582
|
...(goal?.state === "active" &&
|
|
503
583
|
origin?.extensionId === goalContract.id &&
|
|
@@ -516,13 +596,15 @@ export const goalExtension = defineExtension({
|
|
|
516
596
|
schema: goalContract.projection.schema,
|
|
517
597
|
read: (ctx, sessionId) => {
|
|
518
598
|
const goal = Option.getOrUndefined(ctx.state.goals.peek(sessionId));
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
599
|
+
const endings = Option.getOrUndefined(ctx.state.endings.peek(sessionId));
|
|
600
|
+
if (goal === undefined && endings === undefined)
|
|
601
|
+
return Option.none();
|
|
602
|
+
const ended = endings?.ended ?? [];
|
|
603
|
+
return Option.some({
|
|
604
|
+
goal: goal === undefined ? null : committed(goal),
|
|
605
|
+
ended,
|
|
606
|
+
omitted: (endings?.total ?? 0) - ended.length,
|
|
607
|
+
});
|
|
526
608
|
},
|
|
527
609
|
},
|
|
528
610
|
hooks: {
|
|
@@ -535,7 +617,7 @@ export const goalExtension = defineExtension({
|
|
|
535
617
|
goal: ({ sessionId, input }) => humanGoal(ctx, sessionId, input),
|
|
536
618
|
pause: ({ sessionId }) => pauseGoal(ctx, sessionId),
|
|
537
619
|
resume: ({ sessionId }) => resumeGoal(ctx, sessionId),
|
|
538
|
-
clear: ({ sessionId }) => clearGoal(ctx, sessionId),
|
|
620
|
+
clear: ({ sessionId }) => clearGoal(ctx, sessionId).pipe(Effect.as({})),
|
|
539
621
|
set: ({ sessionId, input }) => setGoal(ctx, sessionId, input),
|
|
540
622
|
}),
|
|
541
623
|
},
|
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
|
};
|