@fungi.computer/truffle 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/local-board-agents.d.ts +46 -0
- package/dist/local-board-agents.js +138 -0
- package/dist/local-board-shipping.d.ts +129 -0
- package/dist/local-board-shipping.js +325 -0
- package/dist/local-extensions.d.ts +2 -0
- package/dist/local-extensions.js +13 -3
- package/dist/local-login.js +33 -25
- package/dist/private-local-agent-control.d.ts +5 -1
- package/dist/private-local-agent-control.js +11 -1
- package/dist/private-local-auth-rpc.d.ts +5 -0
- package/dist/private-local-auth-rpc.js +46 -4
- package/dist/private-local-client-version.js +3 -1
- package/dist/private-local-delegation.d.ts +18 -0
- package/dist/private-local-delegation.js +70 -16
- package/dist/private-local-runtime-host.js +52 -25
- package/dist/private-local-service-contract.d.ts +1 -0
- package/dist/private-local-service-owner.js +61 -2
- package/dist/private-local-service.js +7 -1
- package/dist/private-local-session-discovery.d.ts +10 -4
- package/dist/private-local-session-discovery.js +37 -4
- package/dist/private-provider-rejection.d.ts +1 -1
- package/dist/private-provider-rejection.js +28 -8
- package/dist/private-sqlite-owner.d.ts +7 -1
- package/dist/private-sqlite-owner.js +14 -2
- package/package.json +22 -13
package/dist/local-extensions.js
CHANGED
|
@@ -37,6 +37,11 @@ const manifestSchema = z.strictObject({
|
|
|
37
37
|
id: identifier.max(64),
|
|
38
38
|
name: z.string().trim().min(1).max(64),
|
|
39
39
|
description: z.string().max(200).default(""),
|
|
40
|
+
/** A shell key that opens this program beside the focused pane. */
|
|
41
|
+
binding: z
|
|
42
|
+
.string()
|
|
43
|
+
.regex(/^<leader>[a-z]$/u)
|
|
44
|
+
.optional(),
|
|
40
45
|
/** Package-relative executable and its arguments. */
|
|
41
46
|
command: z
|
|
42
47
|
.array(z.string().min(1).max(1_024))
|
|
@@ -60,6 +65,7 @@ const terminalRecord = z.strictObject({
|
|
|
60
65
|
id: z.string(),
|
|
61
66
|
name: z.string(),
|
|
62
67
|
description: z.string(),
|
|
68
|
+
binding: z.string().optional(),
|
|
63
69
|
command: z.array(z.string()),
|
|
64
70
|
});
|
|
65
71
|
const revisionRecord = z.strictObject({
|
|
@@ -614,8 +620,8 @@ export function localExtensions(options) {
|
|
|
614
620
|
if (record.client !== null)
|
|
615
621
|
clients.push(record.client);
|
|
616
622
|
const runtime = await runtimeDirectory(project, id);
|
|
617
|
-
for (const terminal of record.terminals)
|
|
618
|
-
|
|
623
|
+
for (const terminal of record.terminals) {
|
|
624
|
+
const launcher = {
|
|
619
625
|
extensionId: id,
|
|
620
626
|
terminalId: terminal.id,
|
|
621
627
|
name: terminal.name,
|
|
@@ -630,7 +636,11 @@ export function localExtensions(options) {
|
|
|
630
636
|
FUNGI_PROJECT_ROOT: project,
|
|
631
637
|
FUNGI_EXTENSION_RUNTIME_DIR: runtime,
|
|
632
638
|
},
|
|
633
|
-
}
|
|
639
|
+
};
|
|
640
|
+
terminals.push(terminal.binding === undefined
|
|
641
|
+
? launcher
|
|
642
|
+
: { ...launcher, binding: terminal.binding });
|
|
643
|
+
}
|
|
634
644
|
}
|
|
635
645
|
return { clients, terminals, diagnostics };
|
|
636
646
|
},
|
package/dist/local-login.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { interactionTitleLimit } from "@fungi.computer/whistle/wire";
|
|
1
2
|
/** Display order for popular model providers; labels remain owned by Pi. */
|
|
2
3
|
export const localPopularProviderIds = Object.freeze([
|
|
3
4
|
"fungi",
|
|
@@ -54,13 +55,32 @@ function notice(event) {
|
|
|
54
55
|
return { type: "progress", message: event.message };
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Pi's prompt text is provider wording, not a modal title: Whistle titles are
|
|
60
|
+
* at most `interactionTitleLimit` characters, and Pi 1.3's Anthropic paste
|
|
61
|
+
* prompt is longer. A message that fits stays the title; a longer one keeps
|
|
62
|
+
* every word as the summary under Fungi's fixed title for that prompt kind,
|
|
63
|
+
* so a provider sentence can never make the dialog refuse to open.
|
|
64
|
+
*/
|
|
65
|
+
function modalText(message, fallbackTitle, hint) {
|
|
66
|
+
const fits = message.trim() !== "" && message.length <= interactionTitleLimit;
|
|
67
|
+
const lines = [
|
|
68
|
+
...(fits ? [] : [message]),
|
|
69
|
+
...(hint === undefined ? [] : [hint]),
|
|
70
|
+
];
|
|
71
|
+
const title = fits ? message : fallbackTitle;
|
|
72
|
+
return lines.length === 0
|
|
73
|
+
? { message: title }
|
|
74
|
+
: { message: title, summary: lines.join(" ") };
|
|
75
|
+
}
|
|
76
|
+
const placeholderOf = (placeholder) => placeholder === undefined ? {} : { placeholder };
|
|
57
77
|
async function ask(interaction, prompt) {
|
|
58
78
|
const options = prompt.signal === undefined ? {} : { signal: prompt.signal };
|
|
59
79
|
switch (prompt.type) {
|
|
60
80
|
case "select":
|
|
61
81
|
return interaction.prompt({
|
|
62
82
|
type: "choice",
|
|
63
|
-
|
|
83
|
+
...modalText(prompt.message, "Choose a sign-in option"),
|
|
64
84
|
options: prompt.options.map((option) => option.description === undefined
|
|
65
85
|
? { value: option.id, label: option.label }
|
|
66
86
|
: {
|
|
@@ -69,36 +89,24 @@ async function ask(interaction, prompt) {
|
|
|
69
89
|
description: option.description,
|
|
70
90
|
}),
|
|
71
91
|
}, options);
|
|
72
|
-
case "manual_code":
|
|
92
|
+
case "manual_code":
|
|
73
93
|
// Generic across every provider that falls back to a pasted code or
|
|
74
94
|
// redirect URL (OpenAI, Anthropic): the provider's own
|
|
75
95
|
// message explains what to paste; this adds the SSH hint once, here,
|
|
76
96
|
// instead of baking it into one provider's registry label.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
type: "text",
|
|
83
|
-
message,
|
|
84
|
-
summary,
|
|
85
|
-
placeholder: prompt.placeholder,
|
|
86
|
-
}, options);
|
|
87
|
-
}
|
|
97
|
+
return interaction.prompt({
|
|
98
|
+
type: "text",
|
|
99
|
+
...modalText(prompt.message, "Paste the sign-in code or redirect URL", "Connecting over SSH? Paste the URL here instead of opening a browser."),
|
|
100
|
+
...placeholderOf(prompt.placeholder),
|
|
101
|
+
}, options);
|
|
88
102
|
case "secret":
|
|
89
103
|
case "text":
|
|
90
|
-
return interaction.prompt(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
: {
|
|
97
|
-
type: "text",
|
|
98
|
-
message: prompt.message,
|
|
99
|
-
placeholder: prompt.placeholder,
|
|
100
|
-
secret: prompt.type === "secret",
|
|
101
|
-
}, options);
|
|
104
|
+
return interaction.prompt({
|
|
105
|
+
type: "text",
|
|
106
|
+
...modalText(prompt.message, prompt.type === "secret" ? "Enter your key" : "Enter a value"),
|
|
107
|
+
...placeholderOf(prompt.placeholder),
|
|
108
|
+
secret: prompt.type === "secret",
|
|
109
|
+
}, options);
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
const methodNotOffered = "The provider did not offer that sign-in method";
|
|
@@ -36,11 +36,13 @@ export type LocalArchiveRequest = Readonly<{
|
|
|
36
36
|
* creating Agent, so the directory owner can hold the created Agent to what
|
|
37
37
|
* the creator holds; its archive names who archives.
|
|
38
38
|
*/
|
|
39
|
-
export type LocalAgentControlDirectory = Omit<AgentControlDirectory, "create" | "list" | "archive" | "unarchive"> & Readonly<{
|
|
39
|
+
export type LocalAgentControlDirectory = Omit<AgentControlDirectory, "create" | "list" | "archive" | "unarchive" | "answer" | "escalate"> & Readonly<{
|
|
40
40
|
list: (signal: AbortSignal, options?: Readonly<{
|
|
41
41
|
archived?: boolean;
|
|
42
42
|
}>) => Promise<readonly LocalAgentControlAgent[]>;
|
|
43
43
|
create: (input: LocalCreateRequest, signal: AbortSignal) => Promise<AgentControlAgent>;
|
|
44
|
+
answer: (input: Parameters<NonNullable<AgentControlDirectory["answer"]>>[0], senderAgentId: string, senderSessionId: string, signal: AbortSignal) => ReturnType<NonNullable<AgentControlDirectory["answer"]>>;
|
|
45
|
+
escalate: (input: Parameters<NonNullable<AgentControlDirectory["escalate"]>>[0], senderAgentId: string, senderSessionId: string, signal: AbortSignal) => ReturnType<NonNullable<AgentControlDirectory["escalate"]>>;
|
|
44
46
|
archive: (input: LocalArchiveRequest, signal: AbortSignal) => Promise<void>;
|
|
45
47
|
unarchive: (input: LocalArchiveRequest, signal: AbortSignal) => Promise<void>;
|
|
46
48
|
}>;
|
|
@@ -75,6 +77,8 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
|
|
|
75
77
|
}>) => Promise<readonly AgentControlAgent[]>;
|
|
76
78
|
send?: (input: import("@fungi.computer/shiitake/agent-control").AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: import("@fungi.computer/shiitake/agent-control").AgentDelegationInvocation) => Promise<import("@fungi.computer/shiitake").Accepted | import("@fungi.computer/shiitake").Held>;
|
|
77
79
|
recall?: (input: import("@fungi.computer/shiitake/agent-control").AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake").CancelResult>;
|
|
80
|
+
answer?: (input: import("@fungi.computer/shiitake/agent-control").AgentControlAnswerInput, senderSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake/agent-control").AgentControlAnswerOutput>;
|
|
81
|
+
escalate?: (input: import("@fungi.computer/shiitake/agent-control").AgentControlEscalateInput, senderSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake/agent-control").AgentControlEscalateOutput>;
|
|
78
82
|
create?: (input: Readonly<{
|
|
79
83
|
requestId: string;
|
|
80
84
|
name?: string;
|
|
@@ -8,7 +8,7 @@ import { createConversationAgents } from "@fungi.computer/shiitake-extensions/ro
|
|
|
8
8
|
export function createPrivateLocalAgentControl(input) {
|
|
9
9
|
const conversations = createConversationAgents(input.storage);
|
|
10
10
|
function bound(parentSessionId) {
|
|
11
|
-
const { create, archive, unarchive } = input.directory;
|
|
11
|
+
const { create, archive, unarchive, answer, escalate } = input.directory;
|
|
12
12
|
const by = { kind: "agent", agentId: input.agentId };
|
|
13
13
|
return {
|
|
14
14
|
...input.directory,
|
|
@@ -23,6 +23,16 @@ export function createPrivateLocalAgentControl(input) {
|
|
|
23
23
|
createdBySessionId: parentSessionId,
|
|
24
24
|
}, signal);
|
|
25
25
|
},
|
|
26
|
+
answer: (request, sourceSessionId, signal) => parentSessionId === undefined
|
|
27
|
+
? Promise.resolve({ status: "refused", reason: "no_provenance" })
|
|
28
|
+
: sourceSessionId !== parentSessionId
|
|
29
|
+
? Promise.resolve({ status: "refused", reason: "not_sender" })
|
|
30
|
+
: answer(request, input.agentId, parentSessionId, signal),
|
|
31
|
+
escalate: (request, sourceSessionId, signal) => parentSessionId === undefined
|
|
32
|
+
? Promise.resolve({ status: "refused", reason: "no_provenance" })
|
|
33
|
+
: sourceSessionId !== parentSessionId
|
|
34
|
+
? Promise.resolve({ status: "refused", reason: "not_sender" })
|
|
35
|
+
: escalate(request, input.agentId, parentSessionId, signal),
|
|
26
36
|
};
|
|
27
37
|
}
|
|
28
38
|
const control = conversations.control(bound);
|
|
@@ -87,8 +87,13 @@ declare const wireResultSchema: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.
|
|
|
87
87
|
category: z.ZodLiteral<"cancelled">;
|
|
88
88
|
}, z.core.$strict>, z.ZodObject<{
|
|
89
89
|
category: z.ZodLiteral<"no-key-entered">;
|
|
90
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
91
|
+
category: z.ZodLiteral<"prompt-failed">;
|
|
92
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
93
|
+
category: z.ZodLiteral<"missing-authorization-code">;
|
|
90
94
|
}, z.core.$strict>, z.ZodObject<{
|
|
91
95
|
category: z.ZodLiteral<"unknown">;
|
|
96
|
+
errorName: z.ZodString;
|
|
92
97
|
}, z.core.$strict>], "category">;
|
|
93
98
|
}, z.core.$strict>]>;
|
|
94
99
|
declare class AuthPromptTarget extends RpcTarget {
|
|
@@ -80,6 +80,24 @@ const eventSchema = z.discriminatedUnion("type", [
|
|
|
80
80
|
}),
|
|
81
81
|
]);
|
|
82
82
|
const answerSchema = z.string().max(localAuthInputLimit);
|
|
83
|
+
const errorNameSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/u);
|
|
84
|
+
/** A class name only: anything else (or a non-Error) is named generically. */
|
|
85
|
+
function errorNameOf(cause) {
|
|
86
|
+
if (!(cause instanceof Error))
|
|
87
|
+
return "NonError";
|
|
88
|
+
const name = errorNameSchema.safeParse(cause.name);
|
|
89
|
+
return name.success ? name.data : "Error";
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The person's own dialog could not show or answer a provider question (for
|
|
93
|
+
* example the prompt failed the renderer's contract). Fixed text, no cause.
|
|
94
|
+
*/
|
|
95
|
+
class LocalAuthPromptFailed extends Error {
|
|
96
|
+
constructor() {
|
|
97
|
+
super("Authentication prompt failed");
|
|
98
|
+
this.name = "LocalAuthPromptFailed";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
83
101
|
/**
|
|
84
102
|
* The closed set of failures that cross the service. Provider errors can
|
|
85
103
|
* contain credential material, so no free text, cause or provider message
|
|
@@ -98,7 +116,13 @@ const localAuthFailureSchema = z.discriminatedUnion("category", [
|
|
|
98
116
|
}),
|
|
99
117
|
z.strictObject({ category: z.literal("cancelled") }),
|
|
100
118
|
z.strictObject({ category: z.literal("no-key-entered") }),
|
|
101
|
-
z.strictObject({ category: z.literal("
|
|
119
|
+
z.strictObject({ category: z.literal("prompt-failed") }),
|
|
120
|
+
z.strictObject({ category: z.literal("missing-authorization-code") }),
|
|
121
|
+
z.strictObject({
|
|
122
|
+
category: z.literal("unknown"),
|
|
123
|
+
/** The thrown value's class name only, e.g. `TypeError`; never its message. */
|
|
124
|
+
errorName: errorNameSchema,
|
|
125
|
+
}),
|
|
102
126
|
]);
|
|
103
127
|
const wireResultSchema = z.union([
|
|
104
128
|
authResultSchema,
|
|
@@ -110,6 +134,7 @@ const wireResultSchema = z.union([
|
|
|
110
134
|
/** Pi 1.3.0's fixed error prefixes; never retain a provider body suffix. */
|
|
111
135
|
const callbackPortInUse = /^Port \d+ is in use\b/u;
|
|
112
136
|
const oauthStateMismatch = "OAuth state mismatch";
|
|
137
|
+
const missingAuthorizationCode = "Missing authorization code";
|
|
113
138
|
const openAITokenRequestFailed = /^OpenAI OAuth token request failed \(([1-5]\d{2})\)/u;
|
|
114
139
|
function classifyFailure(cause, aborted) {
|
|
115
140
|
if (aborted)
|
|
@@ -133,7 +158,14 @@ function classifyFailure(cause, aborted) {
|
|
|
133
158
|
return { category: "callback-port-in-use" };
|
|
134
159
|
if (cause instanceof Error && cause.message === oauthStateMismatch)
|
|
135
160
|
return { category: "oauth-state-mismatch" };
|
|
136
|
-
|
|
161
|
+
if (cause instanceof LocalAuthPromptFailed)
|
|
162
|
+
return { category: "prompt-failed" };
|
|
163
|
+
if (cause instanceof Error && cause.message === missingAuthorizationCode)
|
|
164
|
+
return { category: "missing-authorization-code" };
|
|
165
|
+
return {
|
|
166
|
+
category: "unknown",
|
|
167
|
+
errorName: errorNameOf(cause),
|
|
168
|
+
};
|
|
137
169
|
}
|
|
138
170
|
/** Fixed wording per category; the dialog shows exactly this. */
|
|
139
171
|
function failureText(failure) {
|
|
@@ -164,8 +196,12 @@ function failureText(failure) {
|
|
|
164
196
|
return "Authentication cancelled";
|
|
165
197
|
case "no-key-entered":
|
|
166
198
|
return new LocalAuthRefusal().message;
|
|
199
|
+
case "prompt-failed":
|
|
200
|
+
return "The sign-in dialog could not show the provider's question (prompt-failed). Nothing was saved.";
|
|
201
|
+
case "missing-authorization-code":
|
|
202
|
+
return "No authorization code was found in what was pasted. Paste the code or the full redirect URL.";
|
|
167
203
|
case "unknown":
|
|
168
|
-
return
|
|
204
|
+
return `Local authentication failed (unknown: ${failure.errorName}). Nothing was saved.`;
|
|
169
205
|
}
|
|
170
206
|
}
|
|
171
207
|
/** The client-side error for one typed failure; its message is fixed text. */
|
|
@@ -333,7 +369,13 @@ export class PrivateLocalAuthTarget extends RpcTarget {
|
|
|
333
369
|
try {
|
|
334
370
|
if (promptSignal.aborted)
|
|
335
371
|
cancel();
|
|
336
|
-
|
|
372
|
+
const answer = await pending.result().then((value) => ({ value }), (cause) => ({ cause }));
|
|
373
|
+
if ("cause" in answer) {
|
|
374
|
+
promptSignal.throwIfAborted();
|
|
375
|
+
// The presenter's error text is not trusted to be secret-free.
|
|
376
|
+
throw new LocalAuthPromptFailed();
|
|
377
|
+
}
|
|
378
|
+
return answerSchema.parse(answer.value);
|
|
337
379
|
}
|
|
338
380
|
finally {
|
|
339
381
|
promptSignal.removeEventListener("abort", cancel);
|
|
@@ -5,7 +5,9 @@ import { basename, dirname, join } from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
const execute = promisify(execFile);
|
|
8
|
-
|
|
8
|
+
// A compiled launcher cold-starts a Bun process; 2s sat at twice its quiet-box
|
|
9
|
+
// time and killed the query on a loaded box.
|
|
10
|
+
const queryOptions = { timeout: 10_000, maxBuffer: 4_096 };
|
|
9
11
|
const buildId = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
10
12
|
function installedLauncherOnPath() {
|
|
11
13
|
return new Promise((resolve, reject) => {
|
|
@@ -28,9 +28,27 @@ export declare function createPrivateLocalDelegation(input: Readonly<{
|
|
|
28
28
|
name: string;
|
|
29
29
|
}[] | undefined>) | undefined;
|
|
30
30
|
accept: (mail: BirdDogMail, meta?: BirdDogMail["meta"], reply?: VerifiedAgentReply) => Promise<Accepted>;
|
|
31
|
+
/**
|
|
32
|
+
* What happens to items a child left waiting on this Agent when the work
|
|
33
|
+
* that was told about them ends. Without it a host leaves them to expire.
|
|
34
|
+
*/
|
|
35
|
+
unattended?: Readonly<{
|
|
36
|
+
/** Exact settled facts of one command of this Agent's own Sessions. */
|
|
37
|
+
readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* Pass one child item on to a person, as this Agent's Session that was
|
|
40
|
+
* told about it. A child that already moved on answers that and nothing changes.
|
|
41
|
+
*/
|
|
42
|
+
escalate: (item: Readonly<{
|
|
43
|
+
agentId: string;
|
|
44
|
+
sessionId: string;
|
|
45
|
+
itemId: string;
|
|
46
|
+
}>, receiverSessionId: string) => Promise<void>;
|
|
47
|
+
}>;
|
|
31
48
|
}>): {
|
|
32
49
|
send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<DelegationReceipt>;
|
|
33
50
|
recall: (task: AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<CancelResult>;
|
|
34
51
|
receive: (mail: BirdDogMail) => Promise<Accepted>;
|
|
52
|
+
close: () => Promise<void>;
|
|
35
53
|
outstanding: () => Promise<import("@fungi.computer/shiitake/fleet").OutstandingDelegation | undefined>;
|
|
36
54
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
1
2
|
import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
|
|
2
3
|
import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
|
|
3
4
|
/** How long one Local delegation grant stays valid while it is admitted. */
|
|
@@ -31,7 +32,67 @@ export function createPrivateLocalDelegation(input) {
|
|
|
31
32
|
}
|
|
32
33
|
return agents;
|
|
33
34
|
}
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Items a child left waiting on this Agent, by the reply command that told
|
|
37
|
+
* it. When the work that carried the reply ends, whatever the Agent neither
|
|
38
|
+
* answered nor escalated goes to a person, so no waiting item is silent.
|
|
39
|
+
*/
|
|
40
|
+
const unattended = new Map();
|
|
41
|
+
let poll;
|
|
42
|
+
/** Escalate the items of every reply whose carrying work has settled. */
|
|
43
|
+
async function settleUnattended() {
|
|
44
|
+
const handling = input.unattended;
|
|
45
|
+
if (handling === undefined)
|
|
46
|
+
return;
|
|
47
|
+
for (const [commandId, entry] of unattended) {
|
|
48
|
+
const settlement = await handling
|
|
49
|
+
.readSettlement(entry.sessionId, commandId)
|
|
50
|
+
.catch((cause) => {
|
|
51
|
+
// Not settled as far as anyone can tell; try again on the next poll.
|
|
52
|
+
Effect.runFork(Effect.logWarning("delegation.settlement", cause));
|
|
53
|
+
return undefined;
|
|
54
|
+
});
|
|
55
|
+
if (settlement === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
unattended.delete(commandId);
|
|
58
|
+
for (const itemId of entry.items)
|
|
59
|
+
await handling
|
|
60
|
+
.escalate({ ...entry.child, itemId }, entry.sessionId)
|
|
61
|
+
.catch((cause) => {
|
|
62
|
+
Effect.runFork(Effect.logWarning("delegation.escalate", cause));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (unattended.size === 0 && poll !== undefined) {
|
|
66
|
+
clearInterval(poll);
|
|
67
|
+
poll = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function watchUnattended(accepted, receiverSessionId, reply) {
|
|
71
|
+
if (input.unattended === undefined ||
|
|
72
|
+
reply.outcome === "undeliverable" ||
|
|
73
|
+
reply.waitingOnYou === undefined)
|
|
74
|
+
return;
|
|
75
|
+
if (receiverSessionId === undefined)
|
|
76
|
+
return;
|
|
77
|
+
unattended.set(accepted.commandId, {
|
|
78
|
+
sessionId: receiverSessionId,
|
|
79
|
+
child: { agentId: reply.agentId, sessionId: reply.sessionId },
|
|
80
|
+
items: reply.waitingOnYou.map(({ itemId }) => itemId),
|
|
81
|
+
});
|
|
82
|
+
poll ??= setInterval(() => {
|
|
83
|
+
void settleUnattended();
|
|
84
|
+
}, 1_000);
|
|
85
|
+
poll.unref();
|
|
86
|
+
}
|
|
87
|
+
/** Escalate what is owed now and stop watching; the Agent is closing. */
|
|
88
|
+
async function close() {
|
|
89
|
+
await settleUnattended();
|
|
90
|
+
unattended.clear();
|
|
91
|
+
if (poll !== undefined)
|
|
92
|
+
clearInterval(poll);
|
|
93
|
+
poll = undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Send with one caller-owned identity; follow-up and steer replies return here. */
|
|
35
96
|
async function send(task, sourceSessionId, signal, invocation) {
|
|
36
97
|
if (invocation === undefined)
|
|
37
98
|
throw new Error("Delegation requires a tool invocation");
|
|
@@ -43,19 +104,9 @@ export function createPrivateLocalDelegation(input) {
|
|
|
43
104
|
requestId: task.requestId,
|
|
44
105
|
});
|
|
45
106
|
signal.throwIfAborted();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return await opened.agent.session(task.sessionId).send(task.text, {
|
|
50
|
-
requestId,
|
|
51
|
-
delivery: "steer",
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
await opened.release();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const meta = delegationSendMeta({ id: input.agentId, name: input.agentId }, invocation.meta);
|
|
107
|
+
// A person reads this name where work waits on its sender.
|
|
108
|
+
const sender = agents.find(({ agentId }) => agentId === input.agentId);
|
|
109
|
+
const meta = delegationSendMeta({ id: input.agentId, name: sender?.name ?? input.agentId }, invocation.meta);
|
|
59
110
|
// The caller's request key owns replay, not the current model program's bytes.
|
|
60
111
|
const provenance = await delegationProvenance({
|
|
61
112
|
agentId: input.agentId,
|
|
@@ -71,6 +122,7 @@ export function createPrivateLocalDelegation(input) {
|
|
|
71
122
|
}, {
|
|
72
123
|
agentId: task.agentId,
|
|
73
124
|
sessionId: task.sessionId,
|
|
125
|
+
delivery: task.delivery,
|
|
74
126
|
text: task.text,
|
|
75
127
|
requestId,
|
|
76
128
|
});
|
|
@@ -147,7 +199,9 @@ export function createPrivateLocalDelegation(input) {
|
|
|
147
199
|
if (replier === undefined && !authenticatedUndeliverable)
|
|
148
200
|
throw new Error("Local reply sender is outside this project");
|
|
149
201
|
const meta = delegationReplyMeta(delegation, replier?.name ?? delegation.reply.agentId);
|
|
150
|
-
|
|
202
|
+
const accepted = await input.accept({ ...mail, meta }, meta, delegation.reply);
|
|
203
|
+
watchUnattended(accepted, mail.to.sessionId, delegation.reply);
|
|
204
|
+
return accepted;
|
|
151
205
|
}
|
|
152
|
-
return { send, recall, receive, outstanding: fleet.outstanding };
|
|
206
|
+
return { send, recall, receive, close, outstanding: fleet.outstanding };
|
|
153
207
|
}
|
|
@@ -46,7 +46,7 @@ import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js
|
|
|
46
46
|
import { createLocalSkillSource } from "./local-skill-source.js";
|
|
47
47
|
import { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
|
|
48
48
|
import { createPrivateFungiMcpConnection, } from "./private-fungi-mcp-connection.js";
|
|
49
|
-
import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
|
|
49
|
+
import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, storeWriteBusyTimeoutMs, } from "./private-sqlite-owner.js";
|
|
50
50
|
import { createPrivateLocalMailRoom, hasPrivateLocalPendingMail, } from "./private-local-mail.js";
|
|
51
51
|
import { createPrivateLocalDelegation, } from "./private-local-delegation.js";
|
|
52
52
|
import { createPrivateLocalAgentControl, } from "./private-local-agent-control.js";
|
|
@@ -207,6 +207,30 @@ async function assertPrivateLocalReceiptFile(path, kind) {
|
|
|
207
207
|
throw cause;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
function ensureReceiptTable(database) {
|
|
211
|
+
database.exec(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
|
|
213
|
+
request_id TEXT PRIMARY KEY NOT NULL,
|
|
214
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
215
|
+
name TEXT,
|
|
216
|
+
created_by_agent_id TEXT,
|
|
217
|
+
created_by_session_id TEXT
|
|
218
|
+
) WITHOUT ROWID
|
|
219
|
+
`);
|
|
220
|
+
const receiptColumns = database
|
|
221
|
+
.prepare(`PRAGMA table_info(${localCreateReceiptTable})`)
|
|
222
|
+
.all()
|
|
223
|
+
.map((column) => column.name);
|
|
224
|
+
if (!receiptColumns.includes("created_by_agent_id"))
|
|
225
|
+
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_agent_id TEXT`);
|
|
226
|
+
if (!receiptColumns.includes("created_by_session_id"))
|
|
227
|
+
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_session_id TEXT`);
|
|
228
|
+
}
|
|
229
|
+
function readReceiptRow(database, requestId) {
|
|
230
|
+
return database
|
|
231
|
+
.prepare(`SELECT agent_id, created_by_agent_id, created_by_session_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
|
|
232
|
+
.get(requestId);
|
|
233
|
+
}
|
|
210
234
|
/** Resolve an Agent create request through its durable Local receipt. */
|
|
211
235
|
export async function resolvePrivateLocalCreateStore(root, requestId, creator) {
|
|
212
236
|
if (requestId.length === 0 || requestId.length > 256)
|
|
@@ -229,27 +253,13 @@ export async function resolvePrivateLocalCreateStore(root, requestId, creator) {
|
|
|
229
253
|
try {
|
|
230
254
|
database.exec(`PRAGMA busy_timeout = ${String(receiptBusyTimeoutMs)}`);
|
|
231
255
|
database.exec("BEGIN IMMEDIATE");
|
|
232
|
-
database
|
|
233
|
-
|
|
234
|
-
request_id TEXT PRIMARY KEY NOT NULL,
|
|
235
|
-
agent_id TEXT NOT NULL UNIQUE,
|
|
236
|
-
name TEXT,
|
|
237
|
-
created_by_agent_id TEXT,
|
|
238
|
-
created_by_session_id TEXT
|
|
239
|
-
) WITHOUT ROWID
|
|
240
|
-
`);
|
|
241
|
-
const receiptColumns = database
|
|
242
|
-
.prepare(`PRAGMA table_info(${localCreateReceiptTable})`)
|
|
243
|
-
.all()
|
|
244
|
-
.map((column) => column.name);
|
|
245
|
-
if (!receiptColumns.includes("created_by_agent_id"))
|
|
246
|
-
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_agent_id TEXT`);
|
|
247
|
-
if (!receiptColumns.includes("created_by_session_id"))
|
|
248
|
-
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_session_id TEXT`);
|
|
249
|
-
const existing = database
|
|
250
|
-
.prepare(`SELECT agent_id, created_by_agent_id, created_by_session_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
|
|
251
|
-
.get(requestId);
|
|
256
|
+
ensureReceiptTable(database);
|
|
257
|
+
const existing = readReceiptRow(database, requestId);
|
|
252
258
|
if (existing !== undefined) {
|
|
259
|
+
if (creatorAgentId !== undefined &&
|
|
260
|
+
(existing.created_by_agent_id === null ||
|
|
261
|
+
existing.created_by_agent_id === undefined))
|
|
262
|
+
throw new AgentCreateRefused("request_conflict", "the Local Agent create receipt is unbound; use a fresh requestId");
|
|
253
263
|
if (creatorAgentId !== undefined &&
|
|
254
264
|
(existing.created_by_agent_id !== creatorAgentId ||
|
|
255
265
|
existing.created_by_session_id !== creatorSessionId))
|
|
@@ -616,6 +626,13 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
616
626
|
message.stopReason === "error" &&
|
|
617
627
|
modelFailure?.message === message &&
|
|
618
628
|
modelFailure.retryable,
|
|
629
|
+
retryAfterMs: (message) => modelFailure?.message === message &&
|
|
630
|
+
modelFailure.rejection?.code === "rate-limit"
|
|
631
|
+
? modelFailure.rejection.retryAfterMs
|
|
632
|
+
: undefined,
|
|
633
|
+
rejection: (message) => modelFailure?.message === message
|
|
634
|
+
? modelFailure.rejection
|
|
635
|
+
: undefined,
|
|
619
636
|
failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
|
|
620
637
|
providerError: (message) => modelFailure?.message === message
|
|
621
638
|
? modelFailure.providerError
|
|
@@ -637,7 +654,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
637
654
|
const evidence = {
|
|
638
655
|
refusal: turn.refusal(),
|
|
639
656
|
upstream: upstream ?? partial,
|
|
640
|
-
rejection: provider.rejection(),
|
|
657
|
+
rejection: provider.rejection(upstream),
|
|
641
658
|
};
|
|
642
659
|
const message = cause === undefined
|
|
643
660
|
? modelFailureMessage(selected.provider, evidence)
|
|
@@ -674,6 +691,8 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
674
691
|
reason: message,
|
|
675
692
|
retryable: modelFailureRetryable(evidence),
|
|
676
693
|
};
|
|
694
|
+
if (evidence.rejection !== undefined)
|
|
695
|
+
modelFailure.rejection = evidence.rejection;
|
|
677
696
|
if (upstream?.errorMessage !== undefined)
|
|
678
697
|
modelFailure.providerError = upstream.errorMessage;
|
|
679
698
|
}
|
|
@@ -1044,7 +1063,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1044
1063
|
providerCacheKey = createHash("sha256")
|
|
1045
1064
|
.update(JSON.stringify(["fungi-local", paths.databasePath, initialSessionId]))
|
|
1046
1065
|
.digest("hex");
|
|
1047
|
-
database = openPrivateWritableSqliteDatabase(paths.databasePath,
|
|
1066
|
+
database = openPrivateWritableSqliteDatabase(paths.databasePath, storeWriteBusyTimeoutMs);
|
|
1048
1067
|
database.exec("PRAGMA foreign_keys = ON");
|
|
1049
1068
|
const agentId = ensureLocalAgentId({
|
|
1050
1069
|
database,
|
|
@@ -1119,6 +1138,14 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1119
1138
|
return Promise.reject(new Error("Local mail ingress is unavailable"));
|
|
1120
1139
|
return acceptMail(mail, meta, reply);
|
|
1121
1140
|
},
|
|
1141
|
+
unattended: {
|
|
1142
|
+
readSettlement: (sessionId, commandId) => readSettlement === undefined
|
|
1143
|
+
? Promise.reject(new Error("Local settlement reads are unavailable"))
|
|
1144
|
+
: readSettlement(sessionId, commandId),
|
|
1145
|
+
escalate: async (item, receiverSessionId) => {
|
|
1146
|
+
await input.agentControl?.escalate(item, agentId, receiverSessionId, new AbortController().signal);
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1122
1149
|
});
|
|
1123
1150
|
let projectDirectory;
|
|
1124
1151
|
if (input.agentControl !== undefined) {
|
|
@@ -1531,7 +1558,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1531
1558
|
};
|
|
1532
1559
|
},
|
|
1533
1560
|
close: () => {
|
|
1534
|
-
closing ??= closePrivateLocalResources({
|
|
1561
|
+
closing ??= (delegation?.close() ?? Promise.resolve()).then(() => closePrivateLocalResources({
|
|
1535
1562
|
agent,
|
|
1536
1563
|
processes,
|
|
1537
1564
|
sandbox,
|
|
@@ -1539,7 +1566,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1539
1566
|
database,
|
|
1540
1567
|
lease,
|
|
1541
1568
|
providerCacheKey,
|
|
1542
|
-
});
|
|
1569
|
+
}));
|
|
1543
1570
|
return closing;
|
|
1544
1571
|
},
|
|
1545
1572
|
});
|