@fungi.computer/truffle 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +542 -2
- package/computerd-loopback.cjs +16 -0
- package/dist/auth-cli.d.ts +46 -0
- package/dist/auth-cli.js +373 -0
- package/dist/fungi-account-command.d.ts +18 -0
- package/dist/fungi-account-command.js +95 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +23 -0
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +27487 -0
- package/dist/internal/sandbox-metafile.json +6726 -0
- package/dist/linear-auth.d.ts +4 -0
- package/dist/linear-auth.js +72 -0
- package/dist/local-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +143 -0
- package/dist/local-extensions.js +624 -0
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-login.d.ts +55 -0
- package/dist/local-login.js +167 -0
- package/dist/local-models.d.ts +16 -0
- package/dist/local-models.js +82 -0
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +21 -0
- package/dist/local-skill-source.js +433 -0
- package/dist/log-event.d.ts +9 -0
- package/dist/log-event.js +13 -0
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -0
- package/dist/private-bun-shiitake-host.d.ts +11 -0
- package/dist/private-bun-shiitake-host.js +66 -0
- package/dist/private-file-credential-store.d.ts +58 -0
- package/dist/private-file-credential-store.js +331 -0
- package/dist/private-files-patch.d.ts +4 -0
- package/dist/private-files-patch.js +104 -0
- package/dist/private-fungi-account.d.ts +252 -0
- package/dist/private-fungi-account.js +1329 -0
- package/dist/private-fungi-auth-error.d.ts +5 -0
- package/dist/private-fungi-auth-error.js +7 -0
- package/dist/private-fungi-mcp-connection.d.ts +26 -0
- package/dist/private-fungi-mcp-connection.js +162 -0
- package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
- package/dist/private-fungi-mcp-endpoint.js +15 -0
- package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +14 -0
- package/dist/private-fungi-mcp-selection.js +49 -0
- package/dist/private-fungi-model-api.d.ts +6 -0
- package/dist/private-fungi-model-api.js +125 -0
- package/dist/private-fungi-model-catalog.d.ts +16 -0
- package/dist/private-fungi-model-catalog.js +86 -0
- package/dist/private-fungi-model-provider.d.ts +6 -0
- package/dist/private-fungi-model-provider.js +51 -0
- package/dist/private-fungi-pending-logouts.d.ts +34 -0
- package/dist/private-fungi-pending-logouts.js +135 -0
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +58 -0
- package/dist/private-invocation-computer.js +498 -0
- package/dist/private-invocation-files.d.ts +30 -0
- package/dist/private-invocation-files.js +520 -0
- package/dist/private-local-acp-stdio.d.ts +17 -0
- package/dist/private-local-acp-stdio.js +145 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +129 -0
- package/dist/private-local-auth-rpc.js +396 -0
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +49 -0
- package/dist/private-local-computer-attach.js +230 -0
- package/dist/private-local-computer-backend.d.ts +282 -0
- package/dist/private-local-computer-backend.js +402 -0
- package/dist/private-local-computer-processes.d.ts +202 -0
- package/dist/private-local-computer-processes.js +613 -0
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -0
- package/dist/private-local-default-model.d.ts +11 -0
- package/dist/private-local-default-model.js +45 -0
- package/dist/private-local-delegation.d.ts +36 -0
- package/dist/private-local-delegation.js +133 -0
- package/dist/private-local-effective-model.d.ts +27 -0
- package/dist/private-local-effective-model.js +17 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -0
- package/dist/private-local-exec-launch.d.ts +41 -0
- package/dist/private-local-exec-launch.js +51 -0
- package/dist/private-local-exec-path.d.ts +6 -0
- package/dist/private-local-exec-path.js +40 -0
- package/dist/private-local-git-exclude.d.ts +2 -0
- package/dist/private-local-git-exclude.js +33 -0
- package/dist/private-local-http-dispatch.d.ts +5 -0
- package/dist/private-local-http-dispatch.js +14 -0
- package/dist/private-local-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +13 -0
- package/dist/private-local-mail.js +77 -0
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +192 -0
- package/dist/private-local-runtime-host.js +1177 -0
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +170 -0
- package/dist/private-local-service-contract.js +1 -0
- package/dist/private-local-service-discovery.d.ts +217 -0
- package/dist/private-local-service-discovery.js +322 -0
- package/dist/private-local-service-ids.d.ts +3 -0
- package/dist/private-local-service-ids.js +8 -0
- package/dist/private-local-service-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +22 -0
- package/dist/private-local-service-listener.js +407 -0
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +135 -0
- package/dist/private-local-service-owner.js +1369 -0
- package/dist/private-local-service-process.d.ts +1 -0
- package/dist/private-local-service-process.js +8 -0
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +46 -0
- package/dist/private-local-service.js +606 -0
- package/dist/private-local-session-discovery.d.ts +66 -0
- package/dist/private-local-session-discovery.js +278 -0
- package/dist/private-local-shiitake-host.d.ts +44 -0
- package/dist/private-local-shiitake-host.js +143 -0
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +16 -0
- package/dist/private-local-worker-server.js +144 -0
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +39 -0
- package/dist/private-sqlite-owner.js +169 -0
- package/dist/private-team-computers.d.ts +38 -0
- package/dist/private-team-computers.js +110 -0
- package/dist/private-workerd-sandbox.d.ts +3 -0
- package/dist/private-workerd-sandbox.js +347 -0
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +24 -0
- package/dist/team-apps.js +128 -0
- package/dist/tracker-git-credential.d.ts +15 -0
- package/dist/tracker-git-credential.js +87 -0
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/build.json +4037 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-apply-patch +0 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +161 -3
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
|
|
2
|
+
import { observeProviderStatus } from "@fungi.computer/mule";
|
|
3
|
+
import { modelFailureMessage, modelFailureRetryable, } from "./private-provider-rejection.js";
|
|
4
|
+
import { logEvent } from "./log-event.js";
|
|
5
|
+
import { localArchiveHandoff } from "./local-archive-handoff.js";
|
|
6
|
+
import { createMcpFormSession } from "./private-mcp-form-session.js";
|
|
7
|
+
import { localExtensions, } from "./local-extensions.js";
|
|
8
|
+
import { lstat, mkdir, realpath, stat } from "node:fs/promises";
|
|
9
|
+
import { lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { cleanupSessionResources, } from "@earendil-works/pi-ai";
|
|
14
|
+
import { parseModelPreference, } from "@fungi.computer/shiitake/model-preference";
|
|
15
|
+
import { createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
|
|
16
|
+
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
17
|
+
import { words } from "@fungi.computer/cairn/words";
|
|
18
|
+
import { botanicalExtensionsWithSkills } from "@fungi.computer/shiitake-extensions/roster";
|
|
19
|
+
import { homeExtension, homeGrant, } from "@fungi.computer/shiitake-extensions/home/host";
|
|
20
|
+
import { Knapsack } from "@fungi.computer/knapsack";
|
|
21
|
+
import { makeHost, Shiitake, ShiitakeError, } from "@fungi.computer/shiitake";
|
|
22
|
+
import { createPrivateModels } from "./local-models.js";
|
|
23
|
+
import { capturePrivateLocalConfigRoot, } from "./private-local-config-root.js";
|
|
24
|
+
import { acquireSubscriptionTurn } from "./subscription-turn.js";
|
|
25
|
+
import { listOfferedLocalModels } from "./private-local-default-model.js";
|
|
26
|
+
import { ensureAgentDefaultModelTable, readAgentDefaultModel, seedAgentDefaultModel, writeAgentDefaultModel, } from "./private-local-agent-default-model.js";
|
|
27
|
+
import { defaultLocalReasoning, resolveKnownLocalStartupChoice, resolveLocalStartupChoice, startupReasoning, validateLocalReasoning, } from "./private-local-startup-model.js";
|
|
28
|
+
import { assertFungiModelsEnabled } from "./private-fungi-model-api.js";
|
|
29
|
+
import { privateLocalEffectiveModelSchema, } from "./private-local-effective-model.js";
|
|
30
|
+
import { createPrivateInvocationComputer, } from "./private-invocation-computer.js";
|
|
31
|
+
import { createPrivateLocalComputerProcesses, } from "./private-local-computer-processes.js";
|
|
32
|
+
import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
|
|
33
|
+
import { ensurePrivateLocalGitExclude } from "./private-local-git-exclude.js";
|
|
34
|
+
import { createLocalSandboxControls, } from "./private-local-sandbox.js";
|
|
35
|
+
import { localAgentFolderSchema, resolveAgentFolder, } from "./private-local-sandbox-policy.js";
|
|
36
|
+
import { LocalStoreSchemaError } from "./private-local-session-discovery.js";
|
|
37
|
+
import { acquireNodeStoreLease, nodeRuntimeHost, } from "@fungi.computer/shiitake/node";
|
|
38
|
+
import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js";
|
|
39
|
+
import { createLocalSkillSource } from "./local-skill-source.js";
|
|
40
|
+
import { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
|
|
41
|
+
import { createPrivateFungiMcpConnection, } from "./private-fungi-mcp-connection.js";
|
|
42
|
+
import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
|
|
43
|
+
import { createPrivateLocalMailRoom, hasPrivateLocalPendingMail, } from "./private-local-mail.js";
|
|
44
|
+
import { createPrivateLocalDelegation, } from "./private-local-delegation.js";
|
|
45
|
+
import { createPrivateLocalAgentControl, } from "./private-local-agent-control.js";
|
|
46
|
+
import { subagentsExtension, subagentsGrant, } from "@fungi.computer/shiitake-extensions/subagents/host";
|
|
47
|
+
const initialSessionId = "main";
|
|
48
|
+
const maximumInstructionReadBytes = 128 * 1_024;
|
|
49
|
+
const maximumInstructionSnapshotBytes = 32_000;
|
|
50
|
+
const instructionRevisionVersion = "local-host-instructions-v3";
|
|
51
|
+
const authoredInstructions = "You are the local coding agent. Explain durable changes clearly. After a run fails, each new user message is a new instruction: retry or continue the failed task only when the user explicitly asks, and ask what they want when the message is ambiguous.";
|
|
52
|
+
const utf8 = new TextEncoder();
|
|
53
|
+
const localHostIds = defineIdFamily({
|
|
54
|
+
family: "local-host",
|
|
55
|
+
version: 1,
|
|
56
|
+
kinds: {
|
|
57
|
+
store: { prefix: "local_store" },
|
|
58
|
+
"session-create-request": { prefix: "local_session_create_request" },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
function isErrnoException(error) {
|
|
62
|
+
return "code" in error;
|
|
63
|
+
}
|
|
64
|
+
function sidecarPathFor(databasePath) {
|
|
65
|
+
return `${databasePath}.owner.sqlite`;
|
|
66
|
+
}
|
|
67
|
+
function ensureLocalAgentId(database, databasePath, pathAgentId, name, createdBy, folder, requireSandbox) {
|
|
68
|
+
database.exec("BEGIN IMMEDIATE");
|
|
69
|
+
try {
|
|
70
|
+
database.exec(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS private_local_identity (
|
|
72
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
73
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
74
|
+
name TEXT,
|
|
75
|
+
created_by TEXT,
|
|
76
|
+
archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1)),
|
|
77
|
+
folder TEXT NOT NULL DEFAULT '.',
|
|
78
|
+
require_sandbox INTEGER NOT NULL DEFAULT 0 CHECK (require_sandbox IN (0, 1))
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
const columns = database
|
|
82
|
+
.prepare("PRAGMA table_info(private_local_identity)")
|
|
83
|
+
.all();
|
|
84
|
+
if (!["name", "created_by", "archived", "folder", "require_sandbox"].every((required) => columns.some((column) => column.name === required)))
|
|
85
|
+
throw new LocalStoreSchemaError(databasePath);
|
|
86
|
+
const row = database
|
|
87
|
+
.prepare("SELECT agent_id, created_by, folder FROM private_local_identity WHERE singleton = 1")
|
|
88
|
+
.get();
|
|
89
|
+
if (row !== undefined) {
|
|
90
|
+
const parsed = localHostIds.store.schema.safeParse(row.agent_id);
|
|
91
|
+
if (!parsed.success ||
|
|
92
|
+
(pathAgentId !== undefined && parsed.data !== pathAgentId))
|
|
93
|
+
throw new Error("Local Agent identity does not match its store");
|
|
94
|
+
if (createdBy !== undefined && row.created_by !== createdBy)
|
|
95
|
+
throw new Error("Local Agent creation identity conflicts with its store");
|
|
96
|
+
const storedFolder = localAgentFolderSchema.parse(row.folder);
|
|
97
|
+
if (folder !== undefined &&
|
|
98
|
+
localAgentFolderSchema.parse(folder) !== storedFolder)
|
|
99
|
+
throw new Error("Local Agent folder conflicts with its store");
|
|
100
|
+
if (requireSandbox !== undefined)
|
|
101
|
+
database
|
|
102
|
+
.prepare("UPDATE private_local_identity SET require_sandbox = ? WHERE singleton = 1")
|
|
103
|
+
.run(requireSandbox ? 1 : 0);
|
|
104
|
+
database.exec("COMMIT");
|
|
105
|
+
return parsed.data;
|
|
106
|
+
}
|
|
107
|
+
const agentId = pathAgentId ?? localHostIds.store.generate();
|
|
108
|
+
database
|
|
109
|
+
.prepare("INSERT INTO private_local_identity (singleton, agent_id, name, created_by, folder, require_sandbox) VALUES (1, ?, ?, ?, ?, ?)")
|
|
110
|
+
// An unnamed Agent gets a Cairn candidate word name so it never shows its id.
|
|
111
|
+
.run(agentId, name ?? words(), createdBy ?? null, localAgentFolderSchema.parse(folder ?? "."), requireSandbox === true ? 1 : 0);
|
|
112
|
+
database.exec("COMMIT");
|
|
113
|
+
return agentId;
|
|
114
|
+
}
|
|
115
|
+
catch (cause) {
|
|
116
|
+
if (database.isTransaction) {
|
|
117
|
+
try {
|
|
118
|
+
database.exec("ROLLBACK");
|
|
119
|
+
}
|
|
120
|
+
catch (rollbackCause) {
|
|
121
|
+
throw new AggregateError([cause, rollbackCause], "Local transaction and rollback failed", { cause: rollbackCause });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw cause;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Local-owned id for a new Session creation request. */
|
|
128
|
+
export function createPrivateLocalSessionRequestId() {
|
|
129
|
+
return localHostIds["session-create-request"].generate();
|
|
130
|
+
}
|
|
131
|
+
const localCreateReceiptFile = "local-agent-create-receipts.sqlite";
|
|
132
|
+
const localCreateReceiptTable = "local_agent_create_receipts";
|
|
133
|
+
const maximumReceiptIdAttempts = 3;
|
|
134
|
+
const receiptBusyTimeoutMs = 5_000;
|
|
135
|
+
async function ensurePrivateLocalDirectory(path) {
|
|
136
|
+
try {
|
|
137
|
+
await mkdir(path);
|
|
138
|
+
}
|
|
139
|
+
catch (cause) {
|
|
140
|
+
if (!(cause instanceof Error) ||
|
|
141
|
+
!isErrnoException(cause) ||
|
|
142
|
+
cause.code !== "EEXIST")
|
|
143
|
+
throw cause;
|
|
144
|
+
}
|
|
145
|
+
const metadata = await lstat(path);
|
|
146
|
+
const canonical = await realpath(path);
|
|
147
|
+
if (!metadata.isDirectory() ||
|
|
148
|
+
metadata.isSymbolicLink() ||
|
|
149
|
+
canonical !== path)
|
|
150
|
+
throw new Error("Local Agent create directory is not project owned");
|
|
151
|
+
}
|
|
152
|
+
async function assertPrivateLocalReceiptFile(path, kind) {
|
|
153
|
+
// A concurrent create's commit deletes the rollback journal at any point
|
|
154
|
+
// between these reads; a file that is gone has nothing left to check.
|
|
155
|
+
try {
|
|
156
|
+
const metadata = await lstat(path);
|
|
157
|
+
// SQLite can unlink the journal between the kernel's lookup and getattr.
|
|
158
|
+
// Its now-unlinked regular inode is gone, just as with ENOENT below.
|
|
159
|
+
if (kind === "journal" && metadata.isFile() && metadata.nlink === 0)
|
|
160
|
+
return;
|
|
161
|
+
if (!metadata.isFile() ||
|
|
162
|
+
metadata.isSymbolicLink() ||
|
|
163
|
+
metadata.nlink !== 1 ||
|
|
164
|
+
(await realpath(path)) !== path)
|
|
165
|
+
throw new Error("Local Agent create receipt is not a unique project file");
|
|
166
|
+
}
|
|
167
|
+
catch (cause) {
|
|
168
|
+
if (cause instanceof Error &&
|
|
169
|
+
isErrnoException(cause) &&
|
|
170
|
+
cause.code === "ENOENT")
|
|
171
|
+
return;
|
|
172
|
+
throw cause;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Resolve an Agent create request through its durable Local receipt. */
|
|
176
|
+
export async function resolvePrivateLocalCreateStore(root, requestId) {
|
|
177
|
+
if (requestId.length === 0 || requestId.length > 256)
|
|
178
|
+
throw new Error("Local Agent create request id is invalid");
|
|
179
|
+
await ensurePrivateLocalGitExclude(root);
|
|
180
|
+
const fungiDirectory = join(root, ".fungi");
|
|
181
|
+
const agentsDirectory = join(fungiDirectory, "agents");
|
|
182
|
+
await ensurePrivateLocalDirectory(fungiDirectory);
|
|
183
|
+
await ensurePrivateLocalDirectory(agentsDirectory);
|
|
184
|
+
const receiptPath = join(agentsDirectory, localCreateReceiptFile);
|
|
185
|
+
await assertPrivateLocalReceiptFile(receiptPath, "receipt");
|
|
186
|
+
await assertPrivateLocalReceiptFile(`${receiptPath}-journal`, "journal");
|
|
187
|
+
const database = openPrivateWritableSqliteDatabase(receiptPath, receiptBusyTimeoutMs);
|
|
188
|
+
try {
|
|
189
|
+
database.exec(`PRAGMA busy_timeout = ${String(receiptBusyTimeoutMs)}`);
|
|
190
|
+
database.exec("BEGIN IMMEDIATE");
|
|
191
|
+
database.exec(`
|
|
192
|
+
CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
|
|
193
|
+
request_id TEXT PRIMARY KEY NOT NULL,
|
|
194
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
195
|
+
name TEXT
|
|
196
|
+
) WITHOUT ROWID
|
|
197
|
+
`);
|
|
198
|
+
const existing = database
|
|
199
|
+
.prepare(`SELECT agent_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
|
|
200
|
+
.get(requestId);
|
|
201
|
+
if (existing !== undefined) {
|
|
202
|
+
const parsed = localHostIds.store.schema.safeParse(existing.agent_id);
|
|
203
|
+
if (!parsed.success)
|
|
204
|
+
throw new Error("Local Agent create receipt is invalid");
|
|
205
|
+
const agentId = parsed.data;
|
|
206
|
+
database.exec("COMMIT");
|
|
207
|
+
await ensurePrivateLocalDirectory(join(agentsDirectory, agentId));
|
|
208
|
+
return {
|
|
209
|
+
agentId,
|
|
210
|
+
databasePath: join(agentsDirectory, agentId, "agent.sqlite"),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
let agentId;
|
|
214
|
+
const insert = database.prepare(`INSERT OR IGNORE INTO ${localCreateReceiptTable} (request_id, agent_id) VALUES (?, ?)`);
|
|
215
|
+
for (let attempt = 0; attempt < maximumReceiptIdAttempts; attempt += 1) {
|
|
216
|
+
const candidate = localHostIds.store.generate();
|
|
217
|
+
const candidateDirectory = join(agentsDirectory, candidate);
|
|
218
|
+
try {
|
|
219
|
+
// Keep the SQLite write transaction synchronous. Awaiting filesystem
|
|
220
|
+
// work here lets a concurrent same-process retry block the event loop
|
|
221
|
+
// in BEGIN IMMEDIATE, preventing this mkdir continuation from running.
|
|
222
|
+
mkdirSync(candidateDirectory);
|
|
223
|
+
}
|
|
224
|
+
catch (cause) {
|
|
225
|
+
if (cause instanceof Error &&
|
|
226
|
+
isErrnoException(cause) &&
|
|
227
|
+
cause.code === "EEXIST")
|
|
228
|
+
continue;
|
|
229
|
+
throw cause;
|
|
230
|
+
}
|
|
231
|
+
const metadata = lstatSync(candidateDirectory);
|
|
232
|
+
if (!metadata.isDirectory() ||
|
|
233
|
+
metadata.isSymbolicLink() ||
|
|
234
|
+
realpathSync(candidateDirectory) !== candidateDirectory)
|
|
235
|
+
throw new Error("Local Agent create directory is not project owned");
|
|
236
|
+
if (insert.run(requestId, candidate).changes > 0) {
|
|
237
|
+
agentId = candidate;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (agentId === undefined)
|
|
242
|
+
throw new Error("Could not allocate a unique Local Agent identity");
|
|
243
|
+
database.exec("COMMIT");
|
|
244
|
+
return {
|
|
245
|
+
agentId,
|
|
246
|
+
databasePath: join(agentsDirectory, agentId, "agent.sqlite"),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
catch (cause) {
|
|
250
|
+
if (database.isTransaction) {
|
|
251
|
+
try {
|
|
252
|
+
database.exec("ROLLBACK");
|
|
253
|
+
}
|
|
254
|
+
catch (rollbackCause) {
|
|
255
|
+
throw new AggregateError([cause, rollbackCause], "Local transaction and rollback failed", { cause: rollbackCause });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
throw cause;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
database.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function localDatabasePaths(root, requested, createRequestId) {
|
|
265
|
+
if (requested !== undefined && createRequestId !== undefined)
|
|
266
|
+
throw new Error("Local Agent database and create request are mutually exclusive");
|
|
267
|
+
if (requested !== undefined) {
|
|
268
|
+
const existing = await realpath(resolve(root, requested));
|
|
269
|
+
const metadata = await stat(existing);
|
|
270
|
+
if (!metadata.isFile())
|
|
271
|
+
throw new Error(`Local Agent database is not a file: ${requested}`);
|
|
272
|
+
const parent = await realpath(dirname(existing));
|
|
273
|
+
const databasePath = join(parent, basename(existing));
|
|
274
|
+
const parsedAgentId = localHostIds.store.parse(basename(parent));
|
|
275
|
+
const agentId = parsedAgentId.ok ? parsedAgentId.value : undefined;
|
|
276
|
+
return { agentId, databasePath, sidecarPath: sidecarPathFor(databasePath) };
|
|
277
|
+
}
|
|
278
|
+
const createStore = createRequestId === undefined
|
|
279
|
+
? undefined
|
|
280
|
+
: await resolvePrivateLocalCreateStore(root, createRequestId);
|
|
281
|
+
const createDatabasePath = createStore?.databasePath;
|
|
282
|
+
const generatedAgentId = createStore?.agentId ?? localHostIds.store.generate();
|
|
283
|
+
const selectedParent = createDatabasePath === undefined
|
|
284
|
+
? join(root, ".fungi", "agents", generatedAgentId)
|
|
285
|
+
: dirname(createDatabasePath);
|
|
286
|
+
let parent;
|
|
287
|
+
if (createRequestId === undefined) {
|
|
288
|
+
await mkdir(selectedParent, { recursive: true });
|
|
289
|
+
parent = await realpath(selectedParent);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
for (const path of [
|
|
293
|
+
join(root, ".fungi"),
|
|
294
|
+
join(root, ".fungi", "agents"),
|
|
295
|
+
selectedParent,
|
|
296
|
+
]) {
|
|
297
|
+
try {
|
|
298
|
+
await mkdir(path);
|
|
299
|
+
}
|
|
300
|
+
catch (cause) {
|
|
301
|
+
if (!(cause instanceof Error) ||
|
|
302
|
+
!isErrnoException(cause) ||
|
|
303
|
+
cause.code !== "EEXIST")
|
|
304
|
+
throw cause;
|
|
305
|
+
}
|
|
306
|
+
const metadata = await lstat(path);
|
|
307
|
+
const canonical = await realpath(path);
|
|
308
|
+
if (!metadata.isDirectory() ||
|
|
309
|
+
metadata.isSymbolicLink() ||
|
|
310
|
+
canonical !== path)
|
|
311
|
+
throw new Error("Local Agent create directory is not project owned");
|
|
312
|
+
}
|
|
313
|
+
parent = await realpath(selectedParent);
|
|
314
|
+
const withinProject = relative(root, parent);
|
|
315
|
+
if (isAbsolute(withinProject) ||
|
|
316
|
+
withinProject === ".." ||
|
|
317
|
+
withinProject.startsWith("../"))
|
|
318
|
+
throw new Error("Local Agent create directory is outside this project");
|
|
319
|
+
}
|
|
320
|
+
const databasePath = createDatabasePath ?? join(parent, "agent.sqlite");
|
|
321
|
+
if (createRequestId !== undefined) {
|
|
322
|
+
for (const path of [databasePath, sidecarPathFor(databasePath)]) {
|
|
323
|
+
let metadata;
|
|
324
|
+
try {
|
|
325
|
+
metadata = await lstat(path);
|
|
326
|
+
}
|
|
327
|
+
catch (cause) {
|
|
328
|
+
if (!(cause instanceof Error) ||
|
|
329
|
+
!isErrnoException(cause) ||
|
|
330
|
+
cause.code !== "ENOENT")
|
|
331
|
+
throw cause;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (!metadata.isFile() ||
|
|
335
|
+
metadata.isSymbolicLink() ||
|
|
336
|
+
metadata.nlink !== 1)
|
|
337
|
+
throw new Error("Local Agent create store is not a unique project file");
|
|
338
|
+
if ((await realpath(path)) !== path)
|
|
339
|
+
throw new Error("Local Agent create store is not a unique project file");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
agentId: generatedAgentId,
|
|
344
|
+
databasePath,
|
|
345
|
+
sidecarPath: sidecarPathFor(databasePath),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function frame(parts) {
|
|
349
|
+
const total = parts.reduce((size, part) => size + 4 + part.byteLength, 0);
|
|
350
|
+
const framed = new Uint8Array(total);
|
|
351
|
+
const view = new DataView(framed.buffer);
|
|
352
|
+
let offset = 0;
|
|
353
|
+
for (const part of parts) {
|
|
354
|
+
view.setUint32(offset, part.byteLength);
|
|
355
|
+
offset += 4;
|
|
356
|
+
framed.set(part, offset);
|
|
357
|
+
offset += part.byteLength;
|
|
358
|
+
}
|
|
359
|
+
return framed;
|
|
360
|
+
}
|
|
361
|
+
async function instructionRevision(authored, agents) {
|
|
362
|
+
const digest = await crypto.subtle.digest("SHA-256", frame([
|
|
363
|
+
utf8.encode(instructionRevisionVersion),
|
|
364
|
+
utf8.encode(authored),
|
|
365
|
+
Uint8Array.of(agents === undefined ? 0 : 1),
|
|
366
|
+
utf8.encode(agents ?? ""),
|
|
367
|
+
]));
|
|
368
|
+
return `local-instructions-${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
369
|
+
}
|
|
370
|
+
/** Build the fixed source read by Shiitake once for each local run. */
|
|
371
|
+
export function createPrivateLocalInstructionSource(port) {
|
|
372
|
+
return Object.freeze({
|
|
373
|
+
load: async ({ sessionId, signal }) => {
|
|
374
|
+
signal.throwIfAborted();
|
|
375
|
+
let agents;
|
|
376
|
+
try {
|
|
377
|
+
const page = await port.read({ path: "AGENTS.md", offset: 0, limit: maximumInstructionReadBytes }, {
|
|
378
|
+
executionId: `local-instructions/${sessionId}`,
|
|
379
|
+
deadlineAt: null,
|
|
380
|
+
signal,
|
|
381
|
+
});
|
|
382
|
+
if ("image" in page)
|
|
383
|
+
throw new Error("Root AGENTS.md is an image, not instructions");
|
|
384
|
+
if (page.truncated)
|
|
385
|
+
throw new Error("Root AGENTS.md exceeds the 128 KiB instruction bound");
|
|
386
|
+
agents = page.content;
|
|
387
|
+
}
|
|
388
|
+
catch (cause) {
|
|
389
|
+
if (cause instanceof Error &&
|
|
390
|
+
isErrnoException(cause) &&
|
|
391
|
+
cause.code === "ENOENT") {
|
|
392
|
+
agents = undefined;
|
|
393
|
+
}
|
|
394
|
+
else
|
|
395
|
+
throw cause;
|
|
396
|
+
}
|
|
397
|
+
signal.throwIfAborted();
|
|
398
|
+
const text = agents === undefined
|
|
399
|
+
? authoredInstructions
|
|
400
|
+
: `${authoredInstructions}\n\n${agents}`;
|
|
401
|
+
if (utf8.encode(text).byteLength > maximumInstructionSnapshotBytes)
|
|
402
|
+
throw new Error(`Local instructions exceed Shiitake's ${String(maximumInstructionSnapshotBytes)}-byte snapshot bound`);
|
|
403
|
+
return Object.freeze({
|
|
404
|
+
revision: await instructionRevision(authoredInstructions, agents),
|
|
405
|
+
text,
|
|
406
|
+
});
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
export function createPrivateLocalModelSource(models, providerId, modelId, providerCacheKey, selectedOwnerReasoning, defaults) {
|
|
411
|
+
const model = modelId === undefined || providerId === undefined
|
|
412
|
+
? undefined
|
|
413
|
+
: models.getModel(providerId, modelId);
|
|
414
|
+
if (providerId !== undefined && providerId !== "fungi" && model === undefined)
|
|
415
|
+
throw new ShiitakeError("model", "rejected", `Local model is not registered: ${providerId}/${modelId ?? "select with --model"}`);
|
|
416
|
+
if (model !== undefined)
|
|
417
|
+
validateLocalReasoning(model, selectedOwnerReasoning);
|
|
418
|
+
const ownerReasoning = selectedOwnerReasoning ??
|
|
419
|
+
(model === undefined ? undefined : defaultLocalReasoning(model));
|
|
420
|
+
const agentPreference = () => defaults?.agent?.() ??
|
|
421
|
+
(providerId === undefined
|
|
422
|
+
? null
|
|
423
|
+
: {
|
|
424
|
+
providerId,
|
|
425
|
+
modelId: modelId ?? "@fungi/luna",
|
|
426
|
+
});
|
|
427
|
+
const prepareSelection = async (preference, signal) => {
|
|
428
|
+
const startup = await resolveLocalStartupChoice({
|
|
429
|
+
models,
|
|
430
|
+
session: preference,
|
|
431
|
+
agent: agentPreference(),
|
|
432
|
+
signal,
|
|
433
|
+
});
|
|
434
|
+
if (startup === null)
|
|
435
|
+
throw new ShiitakeError("model", "rejected", "No signed-in model provider is available. Run /login, then choose a model with /model.");
|
|
436
|
+
const choice = startup.preference;
|
|
437
|
+
const selectedProvider = choice.providerId;
|
|
438
|
+
if (selectedProvider === "fungi") {
|
|
439
|
+
try {
|
|
440
|
+
assertFungiModelsEnabled();
|
|
441
|
+
}
|
|
442
|
+
catch (cause) {
|
|
443
|
+
throw new ShiitakeError("model", "rejected", "Fungi models are paused. Try again later.", undefined, cause);
|
|
444
|
+
}
|
|
445
|
+
await assertAuthenticated("fungi", signal);
|
|
446
|
+
// Catalog facts choose a model; every stream rechecks live Hub authority.
|
|
447
|
+
// A failed catalog refresh must not hide a dispatch refusal from the transcript.
|
|
448
|
+
if (models.getModels("fungi").length === 0) {
|
|
449
|
+
const result = await models.refresh({ providers: ["fungi"], signal });
|
|
450
|
+
if (result.aborted)
|
|
451
|
+
throw new Error("Fungi model catalog refresh was cancelled");
|
|
452
|
+
const failure = result.errors.get("fungi");
|
|
453
|
+
if (failure !== undefined)
|
|
454
|
+
throw new ShiitakeError("model", "rejected", "Fungi models are unavailable. Sign in with /login or choose another provider with /model.", undefined, failure);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const selectedId = choice.modelId;
|
|
458
|
+
const selected = models.getModel(selectedProvider, selectedId);
|
|
459
|
+
if (selected === undefined)
|
|
460
|
+
throw new ShiitakeError("model", "rejected", `Local model is not registered: ${selectedProvider}/${selectedId}`);
|
|
461
|
+
const reasoning = startupReasoning(selected, choice, ownerReasoning);
|
|
462
|
+
validateLocalReasoning(selected, reasoning);
|
|
463
|
+
return { selected, reasoning };
|
|
464
|
+
};
|
|
465
|
+
const assertAuthenticated = async (provider, signal) => {
|
|
466
|
+
const message = `${provider} is not signed in. Run /login or choose another model with /model.`;
|
|
467
|
+
try {
|
|
468
|
+
if ((await models.checkAuth(provider, { signal })) === undefined)
|
|
469
|
+
throw new ShiitakeError("model", "rejected", message);
|
|
470
|
+
signal.throwIfAborted();
|
|
471
|
+
}
|
|
472
|
+
catch (cause) {
|
|
473
|
+
signal.throwIfAborted();
|
|
474
|
+
throw new ShiitakeError("model", "rejected", message, undefined, cause);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
return Object.freeze({
|
|
478
|
+
assertReady: async (preference, signal) => {
|
|
479
|
+
const { selected } = await prepareSelection(preference, signal);
|
|
480
|
+
await assertAuthenticated(selected.provider, signal);
|
|
481
|
+
},
|
|
482
|
+
effective: async (preference, signal) => {
|
|
483
|
+
signal.throwIfAborted();
|
|
484
|
+
let selection;
|
|
485
|
+
try {
|
|
486
|
+
selection = await prepareSelection(preference, signal);
|
|
487
|
+
}
|
|
488
|
+
catch (cause) {
|
|
489
|
+
if (signal.aborted ||
|
|
490
|
+
(preference?.providerId ?? providerId) !== "fungi")
|
|
491
|
+
throw cause;
|
|
492
|
+
logEvent("fungi-model.status", cause);
|
|
493
|
+
const source = cause instanceof ShiitakeError
|
|
494
|
+
? cause.message
|
|
495
|
+
: "Fungi is unreachable or unavailable. Retry or sign in again.";
|
|
496
|
+
return privateLocalEffectiveModelSchema.parse({
|
|
497
|
+
agentName: "Shiitake",
|
|
498
|
+
providerId: "fungi",
|
|
499
|
+
providerName: "Fungi",
|
|
500
|
+
modelId: preference?.modelId ?? modelId ?? "@fungi/luna",
|
|
501
|
+
modelName: "Fungi",
|
|
502
|
+
credential: { kind: "missing", source },
|
|
503
|
+
label: `Shiitake · Fungi · ${source}`,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const { selected, reasoning } = selection;
|
|
507
|
+
const provider = models.getProvider(selected.provider);
|
|
508
|
+
if (provider === undefined)
|
|
509
|
+
throw new Error("Local model provider is not registered");
|
|
510
|
+
const auth = await models.checkAuth(provider.id, { signal });
|
|
511
|
+
signal.throwIfAborted();
|
|
512
|
+
const source = auth === undefined
|
|
513
|
+
? "Sign in required"
|
|
514
|
+
: auth.source === undefined || auth.source === "OAuth"
|
|
515
|
+
? auth.type === "oauth"
|
|
516
|
+
? `${provider.name} login`
|
|
517
|
+
: "API key"
|
|
518
|
+
: auth.source;
|
|
519
|
+
return privateLocalEffectiveModelSchema.parse({
|
|
520
|
+
agentName: "Shiitake",
|
|
521
|
+
providerId: provider.id,
|
|
522
|
+
providerName: provider.name,
|
|
523
|
+
modelId: selected.id,
|
|
524
|
+
modelName: selected.name,
|
|
525
|
+
credential: { kind: auth?.type ?? "missing", source },
|
|
526
|
+
reasoning,
|
|
527
|
+
label: `Shiitake · ${selected.name} (${reasoning ?? "no reasoning"}) · ${source}`,
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
// Observe the same explicit layers against last-known registry facts.
|
|
531
|
+
// Catalog presence alone cannot identify the signed-in fallback: Pi's
|
|
532
|
+
// availability queries check auth. Leave that choice unknown here.
|
|
533
|
+
describe: ({ signal, preference }) => {
|
|
534
|
+
signal.throwIfAborted();
|
|
535
|
+
const choice = resolveKnownLocalStartupChoice({
|
|
536
|
+
session: preference,
|
|
537
|
+
agent: agentPreference(),
|
|
538
|
+
});
|
|
539
|
+
const selected = choice === null
|
|
540
|
+
? undefined
|
|
541
|
+
: models.getModel(choice.preference.providerId, choice.preference.modelId);
|
|
542
|
+
const contextWindow = selected?.contextWindow;
|
|
543
|
+
return Promise.resolve({
|
|
544
|
+
contextWindow: contextWindow !== undefined && contextWindow > 0
|
|
545
|
+
? contextWindow
|
|
546
|
+
: null,
|
|
547
|
+
});
|
|
548
|
+
},
|
|
549
|
+
acquire: async ({ signal, preference }) => {
|
|
550
|
+
signal.throwIfAborted();
|
|
551
|
+
const { selected, reasoning: selectedReasoning } = await prepareSelection(preference, signal);
|
|
552
|
+
await assertAuthenticated(selected.provider, signal);
|
|
553
|
+
const turn = await acquireSubscriptionTurn(models, selected.provider, signal);
|
|
554
|
+
// One acquired door owns at most its latest failure. Identity ties the
|
|
555
|
+
// description to the exact returned message, not to a provider name or
|
|
556
|
+
// prose that any SDK can supply.
|
|
557
|
+
let modelFailure;
|
|
558
|
+
return Promise.resolve(Object.freeze({
|
|
559
|
+
model: selected,
|
|
560
|
+
retryable: (message) => !signal.aborted &&
|
|
561
|
+
message.stopReason === "error" &&
|
|
562
|
+
modelFailure?.message === message &&
|
|
563
|
+
modelFailure.retryable,
|
|
564
|
+
failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
|
|
565
|
+
stream: (selected, context, options) => {
|
|
566
|
+
modelFailure = undefined;
|
|
567
|
+
const streamOptions = { ...options };
|
|
568
|
+
const reasoning = options?.reasoning ?? selectedReasoning;
|
|
569
|
+
if (reasoning !== undefined)
|
|
570
|
+
streamOptions.reasoning = reasoning;
|
|
571
|
+
const provider = observeProviderStatus(streamOptions);
|
|
572
|
+
const events = createAssistantMessageEventStream();
|
|
573
|
+
const failure = (usage, aborted, upstream) => {
|
|
574
|
+
const evidence = {
|
|
575
|
+
refusal: turn.refusal(),
|
|
576
|
+
upstream,
|
|
577
|
+
rejection: provider.rejection(),
|
|
578
|
+
};
|
|
579
|
+
const message = modelFailureMessage(selected.provider, evidence);
|
|
580
|
+
const result = {
|
|
581
|
+
role: "assistant",
|
|
582
|
+
content: [],
|
|
583
|
+
api: selected.api,
|
|
584
|
+
provider: selected.provider,
|
|
585
|
+
model: selected.id,
|
|
586
|
+
usage,
|
|
587
|
+
stopReason: aborted ? "aborted" : "error",
|
|
588
|
+
timestamp: Date.now(),
|
|
589
|
+
errorMessage: aborted ? "Run cancelled" : message,
|
|
590
|
+
};
|
|
591
|
+
if (!aborted)
|
|
592
|
+
modelFailure = {
|
|
593
|
+
message: result,
|
|
594
|
+
reason: message,
|
|
595
|
+
retryable: modelFailureRetryable(evidence),
|
|
596
|
+
};
|
|
597
|
+
return result;
|
|
598
|
+
};
|
|
599
|
+
void (async () => {
|
|
600
|
+
try {
|
|
601
|
+
const upstream = turn.models.streamSimple(selected, context, {
|
|
602
|
+
...streamOptions,
|
|
603
|
+
fetch: provider.fetch,
|
|
604
|
+
onProviderStreamEvent: provider.onProviderStreamEvent,
|
|
605
|
+
sessionId: providerCacheKey,
|
|
606
|
+
transport: options?.transport ?? "auto",
|
|
607
|
+
});
|
|
608
|
+
for await (const event of upstream) {
|
|
609
|
+
if (event.type === "error") {
|
|
610
|
+
const error = failure(event.error.usage, event.reason === "aborted", event.error);
|
|
611
|
+
events.push({ ...event, error });
|
|
612
|
+
events.end(error);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
events.push(event);
|
|
616
|
+
}
|
|
617
|
+
events.end(await upstream.result());
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
const aborted = (options?.signal ?? signal).aborted;
|
|
621
|
+
const error = failure({
|
|
622
|
+
input: 0,
|
|
623
|
+
output: 0,
|
|
624
|
+
cacheRead: 0,
|
|
625
|
+
cacheWrite: 0,
|
|
626
|
+
totalTokens: 0,
|
|
627
|
+
cost: {
|
|
628
|
+
input: 0,
|
|
629
|
+
output: 0,
|
|
630
|
+
cacheRead: 0,
|
|
631
|
+
cacheWrite: 0,
|
|
632
|
+
total: 0,
|
|
633
|
+
},
|
|
634
|
+
}, aborted);
|
|
635
|
+
events.push({
|
|
636
|
+
type: "error",
|
|
637
|
+
reason: aborted ? "aborted" : "error",
|
|
638
|
+
error,
|
|
639
|
+
});
|
|
640
|
+
events.end(error);
|
|
641
|
+
}
|
|
642
|
+
})();
|
|
643
|
+
return events;
|
|
644
|
+
},
|
|
645
|
+
release: turn.release,
|
|
646
|
+
}));
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/** One local Computer source, resolving its PATH and process scope per Session. */
|
|
651
|
+
function createPrivateLocalComputerSource(root, port, sessionPath, processes, home, sandbox) {
|
|
652
|
+
const id = "local-computer";
|
|
653
|
+
return {
|
|
654
|
+
id,
|
|
655
|
+
acquire: async (context) => {
|
|
656
|
+
const sessionId = context.session?.sessionId;
|
|
657
|
+
if (sessionId === undefined)
|
|
658
|
+
throw new Error("Local Computer requires a Session");
|
|
659
|
+
const path = sessionPath(sessionId) ??
|
|
660
|
+
capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd());
|
|
661
|
+
const selected = await createPrivateInvocationComputer(root, path, processes.scope(sessionId), home, sandbox);
|
|
662
|
+
return Knapsack.computer({
|
|
663
|
+
id,
|
|
664
|
+
name: "local",
|
|
665
|
+
namespace: ["computers"],
|
|
666
|
+
files: {
|
|
667
|
+
port,
|
|
668
|
+
pathInstructions: localPathInstructions(root),
|
|
669
|
+
},
|
|
670
|
+
computer: selected,
|
|
671
|
+
}).acquire(context);
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function localPathInstructions(root) {
|
|
676
|
+
return `Files root: ${root}. Relative paths resolve from this root; absolute paths must remain inside it.`;
|
|
677
|
+
}
|
|
678
|
+
/** Assemble the existing Files, Computer, and workerd owners once per host. */
|
|
679
|
+
export async function createPrivateLocalTools(invocationRoot, fungiMcp, agentControl, sessionPath,
|
|
680
|
+
/** This Agent's store; its owner-enabled installed extensions join the roster. */
|
|
681
|
+
storeDirectory,
|
|
682
|
+
/**
|
|
683
|
+
* This Agent's sub-agent record: its Session-bound Agent-control tools and
|
|
684
|
+
* the view's facts. Present when it can direct other Agents.
|
|
685
|
+
*/
|
|
686
|
+
subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox) {
|
|
687
|
+
const forms = createMcpFormSession();
|
|
688
|
+
const root = await realpath(resolve(invocationRoot));
|
|
689
|
+
const port = await createPrivateInvocationFilesPort(root, undefined, sandbox);
|
|
690
|
+
const processes = createPrivateLocalComputerProcesses(root, configRoot.home);
|
|
691
|
+
const computerSources = sessionPath === undefined
|
|
692
|
+
? [
|
|
693
|
+
Knapsack.computer({
|
|
694
|
+
id: "local-computer",
|
|
695
|
+
name: "local",
|
|
696
|
+
namespace: ["computers"],
|
|
697
|
+
files: {
|
|
698
|
+
port,
|
|
699
|
+
pathInstructions: localPathInstructions(root),
|
|
700
|
+
},
|
|
701
|
+
computer: await createPrivateInvocationComputer(root, undefined, processes.scope("invocation"), configRoot.home, sandbox?.launch),
|
|
702
|
+
}),
|
|
703
|
+
]
|
|
704
|
+
: [
|
|
705
|
+
createPrivateLocalComputerSource(root, port, sessionPath, processes, configRoot.home, sandbox?.launch),
|
|
706
|
+
];
|
|
707
|
+
const installed = configuration?.installed ??
|
|
708
|
+
(storeDirectory === undefined
|
|
709
|
+
? { roster: [], grants: [], skills: [], diagnostics: [] }
|
|
710
|
+
: await localExtensions({ home: configRoot.home }).agent(storeDirectory, root));
|
|
711
|
+
function configure(installed, connection, clientPath) {
|
|
712
|
+
const base = botanicalExtensionsWithSkills(createLocalSkillSource({
|
|
713
|
+
cwd: root,
|
|
714
|
+
home: configRoot.home,
|
|
715
|
+
extensions: installed.skills,
|
|
716
|
+
}));
|
|
717
|
+
for (const diagnostic of installed.diagnostics)
|
|
718
|
+
logEvent("local-extension-load", diagnostic.error);
|
|
719
|
+
const archive = storeDirectory === undefined
|
|
720
|
+
? { roster: [], grants: [] }
|
|
721
|
+
: localArchiveHandoff(storeDirectory, installed.grants);
|
|
722
|
+
const extensions = {
|
|
723
|
+
roster: [
|
|
724
|
+
...base.roster,
|
|
725
|
+
forms.extension,
|
|
726
|
+
homeExtension({
|
|
727
|
+
computers: {
|
|
728
|
+
read: () => [
|
|
729
|
+
{ name: "local", attachmentId: "local-computer", port },
|
|
730
|
+
],
|
|
731
|
+
},
|
|
732
|
+
}),
|
|
733
|
+
...(subagents === undefined
|
|
734
|
+
? []
|
|
735
|
+
: [subagentsExtension({ source: subagents.source })]),
|
|
736
|
+
...archive.roster,
|
|
737
|
+
...installed.roster,
|
|
738
|
+
],
|
|
739
|
+
grants: [
|
|
740
|
+
...base.grants,
|
|
741
|
+
{
|
|
742
|
+
id: forms.extension.manifest.id,
|
|
743
|
+
capabilities: forms.extension.manifest.requests,
|
|
744
|
+
},
|
|
745
|
+
homeGrant,
|
|
746
|
+
...(subagents === undefined ? [] : [subagentsGrant]),
|
|
747
|
+
...archive.grants,
|
|
748
|
+
...installed.grants,
|
|
749
|
+
],
|
|
750
|
+
};
|
|
751
|
+
return Object.freeze({
|
|
752
|
+
extensions,
|
|
753
|
+
tools: Object.freeze({
|
|
754
|
+
execution: Object.freeze({ timeoutMs: 900_000 }),
|
|
755
|
+
sources: Object.freeze([
|
|
756
|
+
...(agentControl === undefined
|
|
757
|
+
? []
|
|
758
|
+
: [subagents?.control ?? createAgentControlSource(agentControl)]),
|
|
759
|
+
...(clientPath === undefined
|
|
760
|
+
? computerSources
|
|
761
|
+
: [
|
|
762
|
+
createPrivateLocalComputerSource(root, port, () => clientPath, processes, configRoot.home, sandbox?.launch),
|
|
763
|
+
]),
|
|
764
|
+
...(connection === undefined
|
|
765
|
+
? []
|
|
766
|
+
: [createPrivateFungiMcpConnection(connection)]),
|
|
767
|
+
]),
|
|
768
|
+
sandbox: createPrivateWorkerdSandbox(),
|
|
769
|
+
}),
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const initial = configure(installed, configuration === undefined ? fungiMcp : configuration.fungiMcp, configuration?.clientPath);
|
|
773
|
+
if (initial.tools === undefined)
|
|
774
|
+
throw new Error("Local tools are unavailable");
|
|
775
|
+
return {
|
|
776
|
+
...initial,
|
|
777
|
+
tools: initial.tools,
|
|
778
|
+
port,
|
|
779
|
+
processes,
|
|
780
|
+
configure,
|
|
781
|
+
bindSession: forms.bindSession,
|
|
782
|
+
presentApproval: forms.presentApproval,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function localRuntimeOwner() {
|
|
786
|
+
const host = nodeRuntimeHost((cause) => {
|
|
787
|
+
logEvent("shiitake_reconciliation_failed", cause);
|
|
788
|
+
});
|
|
789
|
+
let pendingWork;
|
|
790
|
+
return Object.freeze({
|
|
791
|
+
...host,
|
|
792
|
+
runtime: Object.freeze({
|
|
793
|
+
bind: (input) => {
|
|
794
|
+
pendingWork = input.pendingWork;
|
|
795
|
+
return host.runtime.bind(input);
|
|
796
|
+
},
|
|
797
|
+
}),
|
|
798
|
+
settled: async () => {
|
|
799
|
+
if (pendingWork === undefined)
|
|
800
|
+
return false;
|
|
801
|
+
const work = await pendingWork();
|
|
802
|
+
return !work.pending && work.nextWakeAt === null;
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
/** Event-loop adaptation for the one local Shiitake owner; it has no queue policy. */
|
|
807
|
+
export function createPrivateLocalRuntimeHost() {
|
|
808
|
+
return localRuntimeOwner().runtime;
|
|
809
|
+
}
|
|
810
|
+
function asLocalFailure(cause, message) {
|
|
811
|
+
return cause instanceof Error ? cause : new Error(message, { cause });
|
|
812
|
+
}
|
|
813
|
+
function aggregateLocalFailures(failures, message) {
|
|
814
|
+
if (failures.length === 1) {
|
|
815
|
+
const [failure] = failures;
|
|
816
|
+
if (failure === undefined)
|
|
817
|
+
throw new Error("Missing local close failure");
|
|
818
|
+
return failure;
|
|
819
|
+
}
|
|
820
|
+
return new AggregateError(failures, message);
|
|
821
|
+
}
|
|
822
|
+
async function captureCloseFailure(close, message) {
|
|
823
|
+
try {
|
|
824
|
+
await close();
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
827
|
+
catch (cause) {
|
|
828
|
+
return asLocalFailure(cause, message);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Close the fixed local owner in dependency order without surrendering its
|
|
833
|
+
* sidecar before runtime drain and main-database close have succeeded.
|
|
834
|
+
*
|
|
835
|
+
* This source-local seam is intentionally not re-exported by the package; the
|
|
836
|
+
* focused tests use it to falsify injected failure ordering.
|
|
837
|
+
*/
|
|
838
|
+
export async function closePrivateLocalResources(resources, initiatingFailure) {
|
|
839
|
+
const failures = initiatingFailure === undefined ? [] : [initiatingFailure];
|
|
840
|
+
if (resources.agent !== undefined) {
|
|
841
|
+
const failure = await captureCloseFailure(resources.agent.close, "Local Agent close failed");
|
|
842
|
+
if (failure !== undefined)
|
|
843
|
+
failures.push(failure);
|
|
844
|
+
}
|
|
845
|
+
// No tool call runs after the Agent closed: stop its background processes.
|
|
846
|
+
if (resources.processes !== undefined) {
|
|
847
|
+
const failure = await captureCloseFailure(resources.processes.close, "Local Computer processes close failed");
|
|
848
|
+
if (failure !== undefined)
|
|
849
|
+
failures.push(failure);
|
|
850
|
+
}
|
|
851
|
+
if (resources.sandbox !== undefined) {
|
|
852
|
+
const failure = await captureCloseFailure(() => Promise.resolve().then(() => resources.sandbox?.close()), "Local sandbox close failed");
|
|
853
|
+
if (failure !== undefined)
|
|
854
|
+
failures.push(failure);
|
|
855
|
+
}
|
|
856
|
+
const runtimeFailure = await captureCloseFailure(resources.runtime.close, "Local runtime close failed");
|
|
857
|
+
if (runtimeFailure !== undefined) {
|
|
858
|
+
failures.push(runtimeFailure);
|
|
859
|
+
throw aggregateLocalFailures(failures, "Local host close failed");
|
|
860
|
+
}
|
|
861
|
+
const providerCacheKey = resources.providerCacheKey;
|
|
862
|
+
if (providerCacheKey !== undefined) {
|
|
863
|
+
const modelFailure = await captureCloseFailure(() => Promise.resolve().then(() => {
|
|
864
|
+
cleanupSessionResources(providerCacheKey);
|
|
865
|
+
}), "Local provider resources close failed");
|
|
866
|
+
if (modelFailure !== undefined)
|
|
867
|
+
failures.push(modelFailure);
|
|
868
|
+
}
|
|
869
|
+
if (resources.database !== undefined) {
|
|
870
|
+
const databaseFailure = await captureCloseFailure(() => Promise.resolve().then(() => resources.database?.close()), "Local Agent database close failed");
|
|
871
|
+
if (databaseFailure !== undefined) {
|
|
872
|
+
failures.push(databaseFailure);
|
|
873
|
+
throw aggregateLocalFailures(failures, "Local host close failed");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (resources.lease !== undefined) {
|
|
877
|
+
const leaseFailure = await captureCloseFailure(() => Promise.resolve().then(() => resources.lease?.release()), "Local ownership sidecar release failed");
|
|
878
|
+
if (leaseFailure !== undefined)
|
|
879
|
+
failures.push(leaseFailure);
|
|
880
|
+
}
|
|
881
|
+
if (failures.length > 0)
|
|
882
|
+
throw aggregateLocalFailures(failures, "Local host close failed");
|
|
883
|
+
}
|
|
884
|
+
/** Adapt Core's store refusal without reclassifying other opening failures. */
|
|
885
|
+
function localHostOpeningFailure(cause, databasePath) {
|
|
886
|
+
if (cause instanceof ShiitakeError &&
|
|
887
|
+
cause.boundary === "store" &&
|
|
888
|
+
cause.cause === "incompatible")
|
|
889
|
+
return new LocalStoreSchemaError(databasePath, { cause });
|
|
890
|
+
return asLocalFailure(cause, "Local host opening failed");
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Open one private file-backed local Shiitake host and its fixed Session.
|
|
894
|
+
*
|
|
895
|
+
* The returned close owns agent, main database, and sidecar release in that
|
|
896
|
+
* order. It is the only lifecycle the current terminal caller needs to learn.
|
|
897
|
+
*/
|
|
898
|
+
export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
899
|
+
const configRoot = input.configRoot ?? capturePrivateLocalConfigRoot();
|
|
900
|
+
const models = suppliedModels ?? createPrivateModels(configRoot.credentialPath);
|
|
901
|
+
const root = await realpath(resolve(input.invocationRoot));
|
|
902
|
+
await ensurePrivateLocalGitExclude(root);
|
|
903
|
+
const selectedProviderId = input.providerId;
|
|
904
|
+
const selectedModelId = input.modelId;
|
|
905
|
+
if (selectedProviderId !== undefined && selectedProviderId !== "fungi") {
|
|
906
|
+
const model = selectedModelId === undefined
|
|
907
|
+
? undefined
|
|
908
|
+
: models.getModel(selectedProviderId, selectedModelId);
|
|
909
|
+
if (model === undefined)
|
|
910
|
+
throw new ShiitakeError("model", "rejected", `Local model is not registered: ${selectedProviderId}/${selectedModelId ?? "select with --model"}`);
|
|
911
|
+
validateLocalReasoning(model, input.reasoning);
|
|
912
|
+
}
|
|
913
|
+
const paths = await localDatabasePaths(root, input.databasePath, input.createRequestId);
|
|
914
|
+
const runtime = localRuntimeOwner();
|
|
915
|
+
let lease;
|
|
916
|
+
let providerCacheKey;
|
|
917
|
+
let database;
|
|
918
|
+
let agent;
|
|
919
|
+
let processes;
|
|
920
|
+
let sandbox;
|
|
921
|
+
const sessionPaths = new Map();
|
|
922
|
+
let usesServiceConfiguration = input.configuration !== undefined;
|
|
923
|
+
try {
|
|
924
|
+
lease = acquireNodeStoreLease(paths.sidecarPath);
|
|
925
|
+
// Claim only after the lease: a rejected opener must not clean up its live owner.
|
|
926
|
+
providerCacheKey = createHash("sha256")
|
|
927
|
+
.update(JSON.stringify(["fungi-local", paths.databasePath, initialSessionId]))
|
|
928
|
+
.digest("hex");
|
|
929
|
+
database = openPrivateWritableSqliteDatabase(paths.databasePath, 0);
|
|
930
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
931
|
+
const agentId = ensureLocalAgentId(database, paths.databasePath, paths.agentId, input.name, input.createdBy, input.folder, input.requireSandbox);
|
|
932
|
+
const sandboxDatabase = database;
|
|
933
|
+
const readSandboxRequirement = () => z
|
|
934
|
+
.union([z.literal(0), z.literal(1)])
|
|
935
|
+
.parse(sandboxDatabase
|
|
936
|
+
.prepare("SELECT require_sandbox FROM private_local_identity WHERE singleton = 1")
|
|
937
|
+
.get()?.require_sandbox) === 1;
|
|
938
|
+
const selectedFolder = resolveAgentFolder(root, localAgentFolderSchema.parse(database
|
|
939
|
+
.prepare("SELECT folder FROM private_local_identity WHERE singleton = 1")
|
|
940
|
+
.get()?.folder));
|
|
941
|
+
const sandboxOwner = createLocalSandboxControls({
|
|
942
|
+
project: root,
|
|
943
|
+
folder: selectedFolder.folder,
|
|
944
|
+
required: readSandboxRequirement,
|
|
945
|
+
});
|
|
946
|
+
sandbox = sandboxOwner;
|
|
947
|
+
// Both constructors below can resume stored commands before returning.
|
|
948
|
+
// Admission must precede tools, configuration hooks and Agent construction.
|
|
949
|
+
const name = z
|
|
950
|
+
.string()
|
|
951
|
+
.nullable()
|
|
952
|
+
.parse(database
|
|
953
|
+
.prepare("SELECT name FROM private_local_identity WHERE singleton = 1")
|
|
954
|
+
.get()?.name) ?? agentId;
|
|
955
|
+
const modelDatabase = database;
|
|
956
|
+
ensureAgentDefaultModelTable(modelDatabase);
|
|
957
|
+
let initialModel = selectedProviderId === undefined || selectedModelId === undefined
|
|
958
|
+
? null
|
|
959
|
+
: parseModelPreference({
|
|
960
|
+
providerId: selectedProviderId,
|
|
961
|
+
modelId: selectedModelId,
|
|
962
|
+
});
|
|
963
|
+
if (initialModel !== null && input.reasoning !== undefined)
|
|
964
|
+
initialModel = parseModelPreference({
|
|
965
|
+
...initialModel,
|
|
966
|
+
reasoning: input.reasoning,
|
|
967
|
+
});
|
|
968
|
+
seedAgentDefaultModel(modelDatabase, initialModel);
|
|
969
|
+
const readAgentModel = () => readAgentDefaultModel(modelDatabase);
|
|
970
|
+
const store = createPrivateSqliteOwner(database);
|
|
971
|
+
let acceptMail;
|
|
972
|
+
let readSettlement;
|
|
973
|
+
if (input.mail !== undefined && input.agentControl === undefined)
|
|
974
|
+
throw new Error("Local delegation requires the project Agent directory");
|
|
975
|
+
const delegation = input.mail === undefined || input.agentControl === undefined
|
|
976
|
+
? undefined
|
|
977
|
+
: createPrivateLocalDelegation({
|
|
978
|
+
agentId,
|
|
979
|
+
storage: store,
|
|
980
|
+
directory: input.agentControl,
|
|
981
|
+
routing: input.mail,
|
|
982
|
+
meshAdmission: input.meshAdmission === undefined
|
|
983
|
+
? undefined
|
|
984
|
+
: async (recipientAgentId) => input.meshAdmission?.(agentId, recipientAgentId),
|
|
985
|
+
accept: (mail, meta, reply) => {
|
|
986
|
+
if (acceptMail === undefined)
|
|
987
|
+
return Promise.reject(new Error("Local mail ingress is unavailable"));
|
|
988
|
+
return acceptMail(mail, meta, reply);
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
let projectDirectory;
|
|
992
|
+
if (input.agentControl !== undefined) {
|
|
993
|
+
projectDirectory = {
|
|
994
|
+
...input.agentControl,
|
|
995
|
+
models: (signal) => listOfferedLocalModels(models, signal),
|
|
996
|
+
};
|
|
997
|
+
if (delegation !== undefined)
|
|
998
|
+
projectDirectory = {
|
|
999
|
+
...projectDirectory,
|
|
1000
|
+
send: delegation.send,
|
|
1001
|
+
recall: delegation.recall,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
const subagents = projectDirectory === undefined
|
|
1005
|
+
? undefined
|
|
1006
|
+
: createPrivateLocalAgentControl({
|
|
1007
|
+
agentId,
|
|
1008
|
+
directory: projectDirectory,
|
|
1009
|
+
storage: store,
|
|
1010
|
+
});
|
|
1011
|
+
const localTools = await createPrivateLocalTools(selectedFolder.absolute, input.fungiMcp, subagents?.directory, (sessionId) => sessionPaths.get(sessionId), dirname(paths.databasePath), subagents, await input.configuration?.(dirname(paths.databasePath)), configRoot, sandboxOwner);
|
|
1012
|
+
processes = localTools.processes;
|
|
1013
|
+
const sessionProcesses = processes;
|
|
1014
|
+
const modelSource = createPrivateLocalModelSource(models, selectedProviderId, selectedModelId, providerCacheKey, input.reasoning, { agent: readAgentModel });
|
|
1015
|
+
const hostOptions = {
|
|
1016
|
+
store,
|
|
1017
|
+
runtime: runtime.runtime,
|
|
1018
|
+
extensions: localTools.extensions,
|
|
1019
|
+
model: modelSource,
|
|
1020
|
+
instructions: createPrivateLocalInstructionSource(localTools.port),
|
|
1021
|
+
tools: localTools.tools,
|
|
1022
|
+
};
|
|
1023
|
+
if (input.mail === undefined) {
|
|
1024
|
+
agent = await Shiitake.make(hostOptions);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
const mailRoom = createPrivateLocalMailRoom(database, {
|
|
1028
|
+
deliver: input.mail.deliver,
|
|
1029
|
+
scheduleWake: runtime.scheduleWake,
|
|
1030
|
+
});
|
|
1031
|
+
const host = await makeHost(hostOptions, mailRoom);
|
|
1032
|
+
agent = host.agent;
|
|
1033
|
+
acceptMail = host.acceptMail;
|
|
1034
|
+
readSettlement = host.readSettlement;
|
|
1035
|
+
}
|
|
1036
|
+
const sessionOwner = agent;
|
|
1037
|
+
const setAgentModel = async (input, signal) => {
|
|
1038
|
+
const preference = parseModelPreference(input);
|
|
1039
|
+
if (preference === null)
|
|
1040
|
+
throw new ShiitakeError("model", "invalid", "An Agent model is required");
|
|
1041
|
+
await modelSource.assertReady(preference, signal);
|
|
1042
|
+
signal.throwIfAborted();
|
|
1043
|
+
writeAgentDefaultModel(modelDatabase, preference);
|
|
1044
|
+
};
|
|
1045
|
+
// /model in a chat sets this Agent's default and its thinking level; a
|
|
1046
|
+
// Session override is cleared so the running Session follows the default.
|
|
1047
|
+
const getSession = (sessionId, attachment) => {
|
|
1048
|
+
const session = localTools.bindSession(sessionId, sessionOwner.session(sessionId), attachment);
|
|
1049
|
+
return {
|
|
1050
|
+
...session,
|
|
1051
|
+
modelPreference: {
|
|
1052
|
+
read: async () => (await session.modelPreference.read()) ?? readAgentModel(),
|
|
1053
|
+
set: async (input) => {
|
|
1054
|
+
const preference = parseModelPreference(input);
|
|
1055
|
+
if (preference !== null)
|
|
1056
|
+
await setAgentModel(preference, new AbortController().signal);
|
|
1057
|
+
await session.modelPreference.set(null);
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
};
|
|
1062
|
+
const session = getSession(initialSessionId);
|
|
1063
|
+
let closing;
|
|
1064
|
+
return Object.freeze({
|
|
1065
|
+
agentId,
|
|
1066
|
+
name,
|
|
1067
|
+
databasePath: paths.databasePath,
|
|
1068
|
+
session,
|
|
1069
|
+
getSession,
|
|
1070
|
+
presentApproval: localTools.presentApproval,
|
|
1071
|
+
setModel: setAgentModel,
|
|
1072
|
+
setRequireSandbox: (required) => {
|
|
1073
|
+
sandboxDatabase
|
|
1074
|
+
.prepare("UPDATE private_local_identity SET require_sandbox = ? WHERE singleton = 1")
|
|
1075
|
+
.run(required ? 1 : 0);
|
|
1076
|
+
sandboxOwner.assertReady();
|
|
1077
|
+
},
|
|
1078
|
+
sandboxStatus: sandboxOwner.status,
|
|
1079
|
+
setArchived: (archived) => {
|
|
1080
|
+
modelDatabase
|
|
1081
|
+
.prepare("UPDATE private_local_identity SET archived = ? WHERE singleton = 1")
|
|
1082
|
+
.run(archived ? 1 : 0);
|
|
1083
|
+
},
|
|
1084
|
+
assertModelReady: async (sessionId, signal) => {
|
|
1085
|
+
const preference = await sessionOwner
|
|
1086
|
+
.session(sessionId)
|
|
1087
|
+
.modelPreference.read();
|
|
1088
|
+
await modelSource.assertReady(preference, signal);
|
|
1089
|
+
},
|
|
1090
|
+
bindSessionPath: (sessionId, path) => {
|
|
1091
|
+
const selected = parsePrivateLocalExecPath(path);
|
|
1092
|
+
if (usesServiceConfiguration)
|
|
1093
|
+
return true;
|
|
1094
|
+
const previous = sessionPaths.get(sessionId);
|
|
1095
|
+
if (previous !== undefined && previous !== selected)
|
|
1096
|
+
return false;
|
|
1097
|
+
sessionPaths.set(sessionId, selected);
|
|
1098
|
+
return true;
|
|
1099
|
+
},
|
|
1100
|
+
effectiveModel: async (sessionId, signal) => {
|
|
1101
|
+
signal.throwIfAborted();
|
|
1102
|
+
const preference = await sessionOwner
|
|
1103
|
+
.session(sessionId)
|
|
1104
|
+
.modelPreference.read();
|
|
1105
|
+
return modelSource.effective(preference, signal);
|
|
1106
|
+
},
|
|
1107
|
+
listSessions: (after) => sessionOwner.listSessions(after),
|
|
1108
|
+
createSession: (requestId) => sessionOwner.createSession(requestId),
|
|
1109
|
+
deleteSession: async (sessionId) => {
|
|
1110
|
+
const deleted = await sessionOwner.deleteSession(sessionId);
|
|
1111
|
+
// The Session ended: its background processes and logs go with it.
|
|
1112
|
+
await sessionProcesses.disposeScope(sessionId);
|
|
1113
|
+
return deleted;
|
|
1114
|
+
},
|
|
1115
|
+
extensions: sessionOwner.extensions,
|
|
1116
|
+
prepareReload: async (configuration) => {
|
|
1117
|
+
const prepared = await sessionOwner.prepareConfiguration(localTools.configure(configuration.installed, configuration.fungiMcp, configuration.clientPath));
|
|
1118
|
+
return {
|
|
1119
|
+
discard: prepared.discard,
|
|
1120
|
+
commit: () => {
|
|
1121
|
+
prepared.commit();
|
|
1122
|
+
usesServiceConfiguration = true;
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
},
|
|
1126
|
+
acceptMail: (mail) => delegation === undefined
|
|
1127
|
+
? Promise.reject(new Error("Local mail ingress is unavailable"))
|
|
1128
|
+
: delegation.receive(mail),
|
|
1129
|
+
pendingDelegation: () => delegation === undefined
|
|
1130
|
+
? Promise.resolve(undefined)
|
|
1131
|
+
: delegation.outstanding(),
|
|
1132
|
+
readSettlement: (sessionId, commandId) => readSettlement === undefined
|
|
1133
|
+
? Promise.reject(new Error("Local settlement reads are unavailable"))
|
|
1134
|
+
: readSettlement(sessionId, commandId),
|
|
1135
|
+
idle: async () => {
|
|
1136
|
+
if (sessionProcesses.running())
|
|
1137
|
+
return false;
|
|
1138
|
+
if (!(await runtime.settled()))
|
|
1139
|
+
return false;
|
|
1140
|
+
// Delegated work still owed to this Agent is not read here: that read
|
|
1141
|
+
// reopens each child, and the child's reply mail reopens this Agent.
|
|
1142
|
+
let after;
|
|
1143
|
+
do {
|
|
1144
|
+
const page = await sessionOwner.listSessions(after);
|
|
1145
|
+
for (const row of page.sessions)
|
|
1146
|
+
if (row.state === "running" || row.state === "needs_you")
|
|
1147
|
+
return false;
|
|
1148
|
+
after = page.next;
|
|
1149
|
+
} while (after !== undefined);
|
|
1150
|
+
// Reconciliation enqueues replies before advancing its settled cursor.
|
|
1151
|
+
// Observe mail last: an earlier empty outbox can become retryable mail
|
|
1152
|
+
// while we await settlement or Session reads, especially during stop
|
|
1153
|
+
// when delivery cannot reopen a parent. The owner rechecks admissions
|
|
1154
|
+
// and uses after this observation before forgetting the Agent.
|
|
1155
|
+
return (input.mail === undefined ||
|
|
1156
|
+
!(await hasPrivateLocalPendingMail(modelDatabase)));
|
|
1157
|
+
},
|
|
1158
|
+
close: () => {
|
|
1159
|
+
closing ??= closePrivateLocalResources({
|
|
1160
|
+
agent,
|
|
1161
|
+
processes,
|
|
1162
|
+
sandbox,
|
|
1163
|
+
runtime,
|
|
1164
|
+
database,
|
|
1165
|
+
lease,
|
|
1166
|
+
providerCacheKey,
|
|
1167
|
+
});
|
|
1168
|
+
return closing;
|
|
1169
|
+
},
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
catch (cause) {
|
|
1173
|
+
const openingFailure = localHostOpeningFailure(cause, paths.databasePath);
|
|
1174
|
+
await closePrivateLocalResources({ agent, processes, sandbox, runtime, database, lease, providerCacheKey }, openingFailure);
|
|
1175
|
+
throw openingFailure;
|
|
1176
|
+
}
|
|
1177
|
+
}
|