@fungi.computer/shiitake-extensions 0.1.1 → 0.3.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 +25 -18
- package/dist/goal/contract.d.ts +21 -8
- package/dist/goal/contract.js +14 -3
- package/dist/goal/host.d.ts +4 -1
- package/dist/goal/host.js +203 -106
- package/dist/goal/prompts.d.ts +73 -0
- package/dist/goal/prompts.js +98 -0
- package/dist/home/host.d.ts +5 -0
- package/dist/home/host.js +11 -4
- package/dist/internal/headline.d.ts +7 -0
- package/dist/internal/headline.js +18 -0
- package/dist/loop/client.js +1 -1
- package/dist/loop/contract.d.ts +6 -2
- package/dist/loop/contract.js +10 -1
- package/dist/loop/host.js +10 -12
- package/dist/loop/prompts.d.ts +56 -0
- package/dist/loop/prompts.js +54 -0
- package/dist/question/host.js +37 -2
- package/dist/roster/conversation-agents.d.ts +10 -14
- package/dist/roster/conversation-agents.js +128 -39
- package/dist/roster.d.ts +3 -0
- package/dist/roster.js +17 -1
- package/dist/skills/host.js +20 -2
- package/dist/subagents/host.d.ts +4 -2
- package/docs/api/goal/contract.md +5 -2
- package/docs/api/home/host.md +6 -5
- package/docs/api/loop/contract.md +5 -2
- package/package.json +5 -5
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Goal } from "./contract.js";
|
|
2
|
+
/** The `/goal` menu for a live or paused goal. */
|
|
3
|
+
export declare function goalMenuPrompt(goal: Goal, now: number): {
|
|
4
|
+
type: "choice";
|
|
5
|
+
message: string;
|
|
6
|
+
summary: string;
|
|
7
|
+
facts: {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}[];
|
|
11
|
+
details: {
|
|
12
|
+
label: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}[];
|
|
15
|
+
options: ({
|
|
16
|
+
value: string;
|
|
17
|
+
label: string;
|
|
18
|
+
destructive?: never;
|
|
19
|
+
} | {
|
|
20
|
+
value: string;
|
|
21
|
+
label: string;
|
|
22
|
+
destructive: true;
|
|
23
|
+
})[];
|
|
24
|
+
} | {
|
|
25
|
+
body: string;
|
|
26
|
+
bodyLabel: string;
|
|
27
|
+
type: "choice";
|
|
28
|
+
message: string;
|
|
29
|
+
summary: string;
|
|
30
|
+
facts: {
|
|
31
|
+
label: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}[];
|
|
34
|
+
details: {
|
|
35
|
+
label: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}[];
|
|
38
|
+
options: ({
|
|
39
|
+
value: string;
|
|
40
|
+
label: string;
|
|
41
|
+
destructive?: never;
|
|
42
|
+
} | {
|
|
43
|
+
value: string;
|
|
44
|
+
label: string;
|
|
45
|
+
destructive: true;
|
|
46
|
+
})[];
|
|
47
|
+
};
|
|
48
|
+
/** The destructive step after choosing Clear: Keep is first and focused. */
|
|
49
|
+
export declare const clearConfirmation: {
|
|
50
|
+
type: "confirmation";
|
|
51
|
+
message: string;
|
|
52
|
+
summary: string;
|
|
53
|
+
confirmLabel: string;
|
|
54
|
+
cancelLabel: string;
|
|
55
|
+
destructive: true;
|
|
56
|
+
};
|
|
57
|
+
/** The destructive step after typing a replacement goal. */
|
|
58
|
+
export declare function replaceConfirmation(current: Goal, replacement: string): {
|
|
59
|
+
type: "confirmation";
|
|
60
|
+
message: string;
|
|
61
|
+
summary: string;
|
|
62
|
+
facts: {
|
|
63
|
+
label: string;
|
|
64
|
+
value: string;
|
|
65
|
+
}[];
|
|
66
|
+
details: {
|
|
67
|
+
label: string;
|
|
68
|
+
value: string;
|
|
69
|
+
}[];
|
|
70
|
+
confirmLabel: string;
|
|
71
|
+
cancelLabel: string;
|
|
72
|
+
destructive: true;
|
|
73
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { headline, titleLimit } from "../internal/headline.js";
|
|
2
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
3
|
+
/** A start time a person reads: 14:02 today, yesterday 14:02, or a full date. */
|
|
4
|
+
function startedAt(epochMillis, now) {
|
|
5
|
+
const at = new Date(epochMillis);
|
|
6
|
+
const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}`;
|
|
7
|
+
const day = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
8
|
+
const days = Math.round((day(new Date(now)) - day(at)) / 86_400_000);
|
|
9
|
+
if (days === 0)
|
|
10
|
+
return `${clock} today`;
|
|
11
|
+
if (days === 1)
|
|
12
|
+
return `${clock} yesterday`;
|
|
13
|
+
return `${String(at.getFullYear())}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${clock}`;
|
|
14
|
+
}
|
|
15
|
+
/** What happened, by the typed cause and never by the goal's own words. */
|
|
16
|
+
const pausedSummary = {
|
|
17
|
+
person: "You paused this goal.",
|
|
18
|
+
blocked: "The Agent is stuck and needs you.",
|
|
19
|
+
stalled: "No progress for 3 turns, so Fungi paused it.",
|
|
20
|
+
cancelled: "You stopped the goal's turn.",
|
|
21
|
+
failed: "The last turn failed. Resume to try again.",
|
|
22
|
+
"no-terminal": "The goal's last turn ended without a final reply.",
|
|
23
|
+
};
|
|
24
|
+
const activeSummary = "Fungi keeps working until it is done or you pause it.";
|
|
25
|
+
/** The `/goal` menu for a live or paused goal. */
|
|
26
|
+
export function goalMenuPrompt(goal, now) {
|
|
27
|
+
const active = goal.state === "active";
|
|
28
|
+
const lead = active ? "Working on: " : "Goal paused: ";
|
|
29
|
+
const name = headline(goal.objective, titleLimit - lead.length);
|
|
30
|
+
const cause = goal.pauseCause;
|
|
31
|
+
const full = goal.objective.slice(0, 4_000);
|
|
32
|
+
const menu = {
|
|
33
|
+
type: "choice",
|
|
34
|
+
message: `${lead}${name}`,
|
|
35
|
+
summary: active ? activeSummary : pausedSummary[cause?.kind ?? "person"],
|
|
36
|
+
facts: [
|
|
37
|
+
{ label: "Started", value: startedAt(goal.startedAt, now) },
|
|
38
|
+
...(!active && cause?.kind === "blocked" && cause.detail != null
|
|
39
|
+
? [{ label: "Why", value: `“${cause.detail.slice(0, 500)}”` }]
|
|
40
|
+
: []),
|
|
41
|
+
],
|
|
42
|
+
details: [
|
|
43
|
+
{ label: "Goal", value: full },
|
|
44
|
+
...(cause === null
|
|
45
|
+
? []
|
|
46
|
+
: [
|
|
47
|
+
{
|
|
48
|
+
label: "Paused",
|
|
49
|
+
value: cause.detail == null
|
|
50
|
+
? cause.kind
|
|
51
|
+
: `${cause.kind} · ${cause.detail.slice(0, 2_000)}`,
|
|
52
|
+
},
|
|
53
|
+
]),
|
|
54
|
+
{ label: "Started", value: new Date(goal.startedAt).toLocaleString() },
|
|
55
|
+
],
|
|
56
|
+
options: [
|
|
57
|
+
active
|
|
58
|
+
? { value: "pause", label: "Pause goal" }
|
|
59
|
+
: { value: "resume", label: "Resume goal" },
|
|
60
|
+
{ value: "replace", label: "Replace goal…" },
|
|
61
|
+
{ value: "clear-ask", label: "Clear goal…", destructive: true },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
// A one-sentence goal is already the title; only longer text gets a body.
|
|
65
|
+
return full.replace(/\s+/gu, " ").trim() === name
|
|
66
|
+
? menu
|
|
67
|
+
: { ...menu, body: full, bodyLabel: "Goal" };
|
|
68
|
+
}
|
|
69
|
+
/** The destructive step after choosing Clear: Keep is first and focused. */
|
|
70
|
+
export const clearConfirmation = {
|
|
71
|
+
type: "confirmation",
|
|
72
|
+
message: "Clear this goal?",
|
|
73
|
+
summary: "It moves to history and can't be resumed.",
|
|
74
|
+
confirmLabel: "Clear goal",
|
|
75
|
+
cancelLabel: "Keep goal",
|
|
76
|
+
destructive: true,
|
|
77
|
+
};
|
|
78
|
+
/** The destructive step after typing a replacement goal. */
|
|
79
|
+
export function replaceConfirmation(current, replacement) {
|
|
80
|
+
const old = current.objective.slice(0, 4_000);
|
|
81
|
+
const next = replacement.slice(0, 4_000);
|
|
82
|
+
return {
|
|
83
|
+
type: "confirmation",
|
|
84
|
+
message: `Replace the ${current.state === "active" ? "live" : "paused"} goal?`,
|
|
85
|
+
summary: "The current goal moves to history and can't be resumed.",
|
|
86
|
+
facts: [
|
|
87
|
+
{ label: "Current", value: headline(old, 120) },
|
|
88
|
+
{ label: "New", value: headline(next, 120) },
|
|
89
|
+
],
|
|
90
|
+
details: [
|
|
91
|
+
{ label: "Current", value: old },
|
|
92
|
+
{ label: "New", value: next },
|
|
93
|
+
],
|
|
94
|
+
confirmLabel: "Replace and start",
|
|
95
|
+
cancelLabel: "Keep current goal",
|
|
96
|
+
destructive: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
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"));
|
|
@@ -99,14 +100,20 @@ export function homeExtension(options) {
|
|
|
99
100
|
type: "info",
|
|
100
101
|
message: "No Computers attached. Attach one before choosing Home.",
|
|
101
102
|
});
|
|
102
|
-
|
|
103
|
+
const recorded = Option.getOrNull(yield* ctx.state.choice.get("default"));
|
|
104
|
+
const home = computers.find(({ attachmentId }) => attachmentId === recorded?.attachmentId);
|
|
105
|
+
const prompt = {
|
|
103
106
|
type: "choice",
|
|
104
|
-
message: "Choose
|
|
107
|
+
message: "Choose a Home Computer",
|
|
108
|
+
summary: "The Agent reads and edits files here by default.",
|
|
105
109
|
options: computers.map(({ name }) => ({
|
|
106
110
|
value: name,
|
|
107
111
|
label: name,
|
|
108
112
|
})),
|
|
109
|
-
}
|
|
113
|
+
};
|
|
114
|
+
return ask(home === undefined
|
|
115
|
+
? prompt
|
|
116
|
+
: { ...prompt, current: home.name }, {}, "name");
|
|
110
117
|
}
|
|
111
118
|
const selected = computers.find(({ name }) => name === input.name);
|
|
112
119
|
if (selected === undefined)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Longest title a modal carries. */
|
|
2
|
+
export declare const titleLimit = 60;
|
|
3
|
+
/**
|
|
4
|
+
* A short name for long user text: its first line's first sentence, whitespace collapsed
|
|
5
|
+
* and cut at a word to `limit` characters with an ellipsis.
|
|
6
|
+
*/
|
|
7
|
+
export declare function headline(objective: string, limit: number): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Longest title a modal carries. */
|
|
2
|
+
export const titleLimit = 60;
|
|
3
|
+
/**
|
|
4
|
+
* A short name for long user text: its first line's first sentence, whitespace collapsed
|
|
5
|
+
* and cut at a word to `limit` characters with an ellipsis.
|
|
6
|
+
*/
|
|
7
|
+
export function headline(objective, limit) {
|
|
8
|
+
const line = objective
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((part) => part.replace(/\s+/gu, " ").trim())
|
|
11
|
+
.find((part) => part !== "") ?? "";
|
|
12
|
+
const sentence = (/^.*?[.!?](?=\s|$)/u.exec(line)?.[0] ?? line).replace(/[.!?]+$/u, "");
|
|
13
|
+
if (sentence.length <= limit)
|
|
14
|
+
return sentence;
|
|
15
|
+
const cut = sentence.slice(0, Math.max(0, limit - 1));
|
|
16
|
+
const space = cut.lastIndexOf(" ");
|
|
17
|
+
return `${(space > limit / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
18
|
+
}
|
package/dist/loop/client.js
CHANGED
package/dist/loop/contract.d.ts
CHANGED
|
@@ -54,7 +54,9 @@ export declare const LoopCommandInput: Schema.Union<readonly [Schema.Struct<{
|
|
|
54
54
|
readonly requestId: Schema.String;
|
|
55
55
|
}>, Schema.Struct<{
|
|
56
56
|
/** Select an existing-loop control or open new-loop entry. */
|
|
57
|
-
readonly action: Schema.Literals<readonly ["status", "pause", "resume", "stop", "new"]>;
|
|
57
|
+
readonly action: Schema.Literals<readonly ["status", "pause", "resume", "stop", "stop-ask", "new"]>;
|
|
58
|
+
/** The human's answer to "stop this loop?"; absent for a typed stop. */
|
|
59
|
+
readonly confirmed: Schema.optional<Schema.Boolean>;
|
|
58
60
|
}>, Schema.Struct<{}>]>;
|
|
59
61
|
/** Loop's contract: Session-scope controls and one projection slot. */
|
|
60
62
|
export declare const LoopProjection: Schema.Struct<{
|
|
@@ -111,7 +113,9 @@ export declare const loopContract: {
|
|
|
111
113
|
readonly requestId: Schema.String;
|
|
112
114
|
}>, Schema.Struct<{
|
|
113
115
|
/** Select an existing-loop control or open new-loop entry. */
|
|
114
|
-
readonly action: Schema.Literals<readonly ["status", "pause", "resume", "stop", "new"]>;
|
|
116
|
+
readonly action: Schema.Literals<readonly ["status", "pause", "resume", "stop", "stop-ask", "new"]>;
|
|
117
|
+
/** The human's answer to "stop this loop?"; absent for a typed stop. */
|
|
118
|
+
readonly confirmed: Schema.optional<Schema.Boolean>;
|
|
115
119
|
}>, Schema.Struct<{}>]>;
|
|
116
120
|
/** Return the server-owned conversation step. */
|
|
117
121
|
readonly output: Schema.declare<import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1, import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1>;
|
package/dist/loop/contract.js
CHANGED
|
@@ -75,7 +75,16 @@ export const LoopCommandInput = Schema.Union([
|
|
|
75
75
|
}),
|
|
76
76
|
Schema.Struct({
|
|
77
77
|
/** Select an existing-loop control or open new-loop entry. */
|
|
78
|
-
action: Schema.Literals([
|
|
78
|
+
action: Schema.Literals([
|
|
79
|
+
"status",
|
|
80
|
+
"pause",
|
|
81
|
+
"resume",
|
|
82
|
+
"stop",
|
|
83
|
+
"stop-ask",
|
|
84
|
+
"new",
|
|
85
|
+
]),
|
|
86
|
+
/** The human's answer to "stop this loop?"; absent for a typed stop. */
|
|
87
|
+
confirmed: Schema.optional(Schema.Boolean),
|
|
79
88
|
}),
|
|
80
89
|
Schema.Struct({}),
|
|
81
90
|
]);
|
package/dist/loop/host.js
CHANGED
|
@@ -8,9 +8,10 @@ import * as Exit from "effect/Exit";
|
|
|
8
8
|
import * as Option from "effect/Option";
|
|
9
9
|
import * as Schema from "effect/Schema";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
import { ask, catalog, done, freshRequestId } from "../internal/catalog.js";
|
|
11
|
+
import { ask, catalog, declined, done, freshRequestId, } from "../internal/catalog.js";
|
|
12
12
|
import { nextOccurrence } from "../internal/cron.js";
|
|
13
13
|
import { Loop, LoopCommandInput, loopContract } from "./contract.js";
|
|
14
|
+
import { loopMenuPrompt, stopConfirmation } from "./prompts.js";
|
|
14
15
|
/** The rules the model follows; the describe page and a live loop's prompt share them. */
|
|
15
16
|
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
17
|
/** Hashed prose and policy: every constant loop reads comes from here. */
|
|
@@ -521,7 +522,8 @@ function loopLine(loop) {
|
|
|
521
522
|
}
|
|
522
523
|
const loopPrompt = {
|
|
523
524
|
type: "text",
|
|
524
|
-
message: "What should the loop repeat?
|
|
525
|
+
message: "What should the loop repeat?",
|
|
526
|
+
summary: "Start with 5m or 1h for a fixed interval.",
|
|
525
527
|
placeholder: "Check the build and fix what broke",
|
|
526
528
|
};
|
|
527
529
|
/** The human `/loop` conversation, decided from the committed loop. */
|
|
@@ -533,16 +535,7 @@ function humanLoop(ctx, sessionId, input) {
|
|
|
533
535
|
if (!("action" in input)) {
|
|
534
536
|
if (current === undefined || !live)
|
|
535
537
|
return ask(loopPrompt, { action: "start", requestId: yield* freshRequestId }, "text");
|
|
536
|
-
return ask({
|
|
537
|
-
type: "choice",
|
|
538
|
-
message: loopLine(current),
|
|
539
|
-
options: [
|
|
540
|
-
current.state === "active"
|
|
541
|
-
? { value: "pause", label: "Pause loop" }
|
|
542
|
-
: { value: "resume", label: "Resume loop" },
|
|
543
|
-
{ value: "stop", label: "Stop loop" },
|
|
544
|
-
],
|
|
545
|
-
}, {}, "action");
|
|
538
|
+
return ask(loopMenuPrompt(current), {}, "action");
|
|
546
539
|
}
|
|
547
540
|
switch (input.action) {
|
|
548
541
|
case "status":
|
|
@@ -564,7 +557,12 @@ function humanLoop(ctx, sessionId, input) {
|
|
|
564
557
|
type: "success",
|
|
565
558
|
message: loopLine(yield* resumeLoop(ctx, sessionId)),
|
|
566
559
|
});
|
|
560
|
+
case "stop-ask":
|
|
561
|
+
// The menu's Stop: Keep is the focused answer to this destructive step.
|
|
562
|
+
return ask(stopConfirmation, { action: "stop" }, "confirmed");
|
|
567
563
|
case "stop":
|
|
564
|
+
if (input.confirmed === false)
|
|
565
|
+
return declined({ type: "info", message: "Kept the loop." });
|
|
568
566
|
return done({
|
|
569
567
|
type: "success",
|
|
570
568
|
message: loopLine(yield* stopLoop(ctx, sessionId)),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Loop } from "./contract.js";
|
|
2
|
+
/** The `/loop` menu for a live or paused loop. */
|
|
3
|
+
export declare function loopMenuPrompt(loop: Loop): {
|
|
4
|
+
type: "choice";
|
|
5
|
+
message: string;
|
|
6
|
+
summary: string;
|
|
7
|
+
facts: {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}[];
|
|
11
|
+
details: {
|
|
12
|
+
label: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}[];
|
|
15
|
+
options: ({
|
|
16
|
+
value: string;
|
|
17
|
+
label: string;
|
|
18
|
+
destructive?: never;
|
|
19
|
+
} | {
|
|
20
|
+
value: string;
|
|
21
|
+
label: string;
|
|
22
|
+
destructive: true;
|
|
23
|
+
})[];
|
|
24
|
+
} | {
|
|
25
|
+
body: string;
|
|
26
|
+
bodyLabel: string;
|
|
27
|
+
type: "choice";
|
|
28
|
+
message: string;
|
|
29
|
+
summary: string;
|
|
30
|
+
facts: {
|
|
31
|
+
label: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}[];
|
|
34
|
+
details: {
|
|
35
|
+
label: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}[];
|
|
38
|
+
options: ({
|
|
39
|
+
value: string;
|
|
40
|
+
label: string;
|
|
41
|
+
destructive?: never;
|
|
42
|
+
} | {
|
|
43
|
+
value: string;
|
|
44
|
+
label: string;
|
|
45
|
+
destructive: true;
|
|
46
|
+
})[];
|
|
47
|
+
};
|
|
48
|
+
/** The destructive step after choosing Stop: Keep is first and focused. */
|
|
49
|
+
export declare const stopConfirmation: {
|
|
50
|
+
type: "confirmation";
|
|
51
|
+
message: string;
|
|
52
|
+
summary: string;
|
|
53
|
+
confirmLabel: string;
|
|
54
|
+
cancelLabel: string;
|
|
55
|
+
destructive: true;
|
|
56
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { headline, titleLimit } from "../internal/headline.js";
|
|
2
|
+
/** The `/loop` menu for a live or paused loop. */
|
|
3
|
+
export function loopMenuPrompt(loop) {
|
|
4
|
+
const active = loop.state === "active";
|
|
5
|
+
const lead = active ? "Loop running: " : "Loop paused: ";
|
|
6
|
+
const name = headline(loop.prompt, titleLimit - lead.length);
|
|
7
|
+
const full = loop.prompt.slice(0, 4_000);
|
|
8
|
+
const menu = {
|
|
9
|
+
type: "choice",
|
|
10
|
+
message: `${lead}${name}`,
|
|
11
|
+
summary: loop.mode === "self_paced"
|
|
12
|
+
? "The Agent decides when to run it again."
|
|
13
|
+
: "It runs again on a fixed schedule.",
|
|
14
|
+
facts: [
|
|
15
|
+
{
|
|
16
|
+
label: "Mode",
|
|
17
|
+
value: loop.mode === "self_paced" ? "Self-paced" : "Fixed interval",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "Started",
|
|
21
|
+
value: loop.origin === "human" ? "By you" : "By the Agent",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
details: [
|
|
25
|
+
{ label: "Prompt", value: full },
|
|
26
|
+
...(loop.lastError === null
|
|
27
|
+
? []
|
|
28
|
+
: [{ label: "Last error", value: loop.lastError.slice(0, 2_000) }]),
|
|
29
|
+
{
|
|
30
|
+
label: "Failures",
|
|
31
|
+
value: String(loop.consecutiveFailures),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
options: [
|
|
35
|
+
active
|
|
36
|
+
? { value: "pause", label: "Pause loop" }
|
|
37
|
+
: { value: "resume", label: "Resume loop" },
|
|
38
|
+
{ value: "stop-ask", label: "Stop loop…", destructive: true },
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
// A short prompt is already the title; only longer text gets a body.
|
|
42
|
+
return full.replace(/\s+/gu, " ").trim() === name
|
|
43
|
+
? menu
|
|
44
|
+
: { ...menu, body: full, bodyLabel: "Prompt" };
|
|
45
|
+
}
|
|
46
|
+
/** The destructive step after choosing Stop: Keep is first and focused. */
|
|
47
|
+
export const stopConfirmation = {
|
|
48
|
+
type: "confirmation",
|
|
49
|
+
message: "Stop this loop?",
|
|
50
|
+
summary: "It won't run again. You can start a new one.",
|
|
51
|
+
confirmLabel: "Stop loop",
|
|
52
|
+
cancelLabel: "Keep loop",
|
|
53
|
+
destructive: true,
|
|
54
|
+
};
|
package/dist/question/host.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as Schema from "effect/Schema";
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import Papa from "papaparse";
|
|
11
|
+
import { titleLimit } from "../internal/headline.js";
|
|
11
12
|
import { ask, catalog, done, freshRequestId } from "../internal/catalog.js";
|
|
12
13
|
import { questionContract, OpenAsk, Questions, Answers, HumanResult, DeclineInput, ClosedStatus, AnswerFlowInput, } from "./contract.js";
|
|
13
14
|
const config = {
|
|
@@ -695,15 +696,49 @@ function singleSelection(question, selection) {
|
|
|
695
696
|
}))
|
|
696
697
|
: Effect.succeed([option.label]);
|
|
697
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* The Agent's own words are the title only when they fit one; a longer
|
|
701
|
+
* question gets a plain title and sits in the body, whole in the details.
|
|
702
|
+
*/
|
|
703
|
+
function questionHeading(question) {
|
|
704
|
+
const text = question.question.replace(/\s+/gu, " ").trim();
|
|
705
|
+
if (text.length <= titleLimit)
|
|
706
|
+
return { message: text };
|
|
707
|
+
return {
|
|
708
|
+
message: "The Agent has a question",
|
|
709
|
+
body: question.question.slice(0, 4_000),
|
|
710
|
+
bodyLabel: "Question",
|
|
711
|
+
details: [{ label: "Question", value: question.question.slice(0, 4_000) }],
|
|
712
|
+
};
|
|
713
|
+
}
|
|
698
714
|
function promptQuestion(nextQuestion, next) {
|
|
715
|
+
const heading = questionHeading(nextQuestion);
|
|
699
716
|
if (nextQuestion.multiple === true)
|
|
700
717
|
return ask({
|
|
701
718
|
type: "text",
|
|
702
|
-
|
|
719
|
+
...heading,
|
|
720
|
+
summary: `Separate your picks with commas, or leave blank to write your own.${nextQuestion.options.some((option) => option.label.includes(","))
|
|
721
|
+
? " Put quotes around a pick that contains a comma."
|
|
722
|
+
: ""}`,
|
|
723
|
+
facts: [
|
|
724
|
+
{
|
|
725
|
+
label: "Options",
|
|
726
|
+
value: nextQuestion.options
|
|
727
|
+
.map((option) => option.label)
|
|
728
|
+
.join(" · "),
|
|
729
|
+
},
|
|
730
|
+
],
|
|
731
|
+
details: [
|
|
732
|
+
...("details" in heading ? heading.details : []),
|
|
733
|
+
...nextQuestion.options.map((option) => ({
|
|
734
|
+
label: option.label.slice(0, 200),
|
|
735
|
+
value: option.description,
|
|
736
|
+
})),
|
|
737
|
+
].slice(0, 24),
|
|
703
738
|
}, { stage: "multiple", ...next }, "selection");
|
|
704
739
|
return ask({
|
|
705
740
|
type: "choice",
|
|
706
|
-
|
|
741
|
+
...heading,
|
|
707
742
|
options: [
|
|
708
743
|
...nextQuestion.options.map((option) => ({
|
|
709
744
|
value: `option:${option.label}`,
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { type AgentControlDirectory } from "@fungi.computer/shiitake/agent-control";
|
|
2
2
|
import type { CapabilitySource } from "@fungi.computer/mycelium";
|
|
3
3
|
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
4
|
-
import {
|
|
4
|
+
import type { SubagentSource } from "../subagents/host.js";
|
|
5
5
|
/** Host-pack creation admission; attachments/model selection retain their own owners. */
|
|
6
6
|
export declare function assertOrdinaryAgentCreation(input: Parameters<NonNullable<AgentControlDirectory["create"]>>[0]): void;
|
|
7
7
|
/**
|
|
8
8
|
* This host's own record of the Agents each conversation created or sent
|
|
9
9
|
* accepted work to, written as its Agent-control directory accepts them
|
|
10
|
-
* (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view).
|
|
11
|
-
*
|
|
10
|
+
* (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view). Accepted
|
|
11
|
+
* child command receipts are separately reconciled against their owner's
|
|
12
|
+
* settlement records for the parent's goal guard. Neither record grants
|
|
13
|
+
* authority over a child.
|
|
12
14
|
*/
|
|
13
15
|
export declare function createConversationAgents(storage: WatchdogSqliteOwner): {
|
|
14
16
|
/** Bind acceptance recording to this conversation's existing directory. */
|
|
@@ -16,16 +18,10 @@ export declare function createConversationAgents(storage: WatchdogSqliteOwner):
|
|
|
16
18
|
/** Agent-control tools bound to the conversation that acquires them. */
|
|
17
19
|
control: (directoryFor: (parentSessionId: string | undefined) => AgentControlDirectory) => CapabilitySource;
|
|
18
20
|
/** The footer rows for one conversation; archived and unknown Agents leave it. */
|
|
19
|
-
source: (directory: Pick<AgentControlDirectory, "list"
|
|
21
|
+
source: (directory: Pick<AgentControlDirectory, "list"> & Pick<SubagentSource, "open">) => SubagentSource;
|
|
20
22
|
};
|
|
21
23
|
/** Both hosts compose the ordinary roster and its read-only created-Agent footer. */
|
|
22
|
-
export declare function oysterAgentExtensions(source: SubagentSource): {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
grants: Readonly<{
|
|
27
|
-
id: import("@fungi.computer/shiitake/extensions").ExtensionId;
|
|
28
|
-
capabilities: readonly import("@fungi.computer/shiitake").ExtensionCapabilityV0[];
|
|
29
|
-
grantKey?: string;
|
|
30
|
-
}>[];
|
|
31
|
-
};
|
|
24
|
+
export declare function oysterAgentExtensions(source: SubagentSource): Readonly<{
|
|
25
|
+
roster: readonly import("@fungi.computer/shiitake").RosteredExtensionV0[];
|
|
26
|
+
grants: readonly import("@fungi.computer/shiitake").ExtensionGrantV0[];
|
|
27
|
+
}>;
|