@fastagent-sh/fastagent 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bind.d.ts +34 -0
- package/dist/bind.js +74 -0
- package/dist/channels/agentcore-state.js +14 -6
- package/dist/cli/commands/deploy.d.ts +13 -0
- package/dist/cli/commands/deploy.js +7 -1
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.js +15 -4
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +11 -3
- package/dist/cli/commands/tool.js +10 -2
- package/dist/cli/program.js +9 -0
- package/dist/cli/serve.d.ts +26 -2
- package/dist/cli/serve.js +71 -17
- package/dist/cli/shared.d.ts +6 -0
- package/dist/cli/shared.js +14 -0
- package/dist/deploy/agentcore/plan.js +1 -1
- package/dist/deploy/preflight.js +18 -0
- package/dist/engines/pi/config.d.ts +6 -2
- package/dist/engines/pi/config.js +8 -2
- package/dist/engines/pi/create.d.ts +25 -17
- package/dist/engines/pi/create.js +37 -14
- package/dist/engines/pi/harness.d.ts +19 -5
- package/dist/engines/pi/harness.js +3 -5
- package/dist/engines/pi/open.d.ts +2 -2
- package/dist/engines/pi/open.js +1 -1
- package/dist/engines/pi/read-image.d.ts +4 -0
- package/dist/engines/pi/read-image.js +62 -0
- package/dist/engines/pi/search-tools.d.ts +6 -4
- package/dist/engines/pi/search-tools.js +3 -1
- package/dist/engines/pi/session-builder.js +7 -2
- package/dist/engines/pi/tool.d.ts +13 -5
- package/dist/engines/pi/wake-tool.d.ts +3 -3
- package/dist/host/node.d.ts +2 -0
- package/dist/host/node.js +2 -1
- package/dist/pi.d.ts +1 -1
- package/package.json +4 -4
|
@@ -538,7 +538,7 @@ function template(input, translated) {
|
|
|
538
538
|
...envLines,
|
|
539
539
|
];
|
|
540
540
|
if (needsForwarder) {
|
|
541
|
-
lines.push(``, ` ForwarderRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: lambda.amazonaws.com }`, ` Action: sts:AssumeRole`, ` ManagedPolicyArns: [arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]`, ` Policies:`, ` - PolicyName: invoke-runtime`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: bedrock-agentcore:InvokeAgentRuntime`, ` Resource:`, ` - !GetAtt Runtime.AgentRuntimeArn`, ` - !Sub "\${Runtime.AgentRuntimeArn}/*"`, ` - Effect: Allow # mint the presigned URLs the container uses for its state snapshot`, ` Action: [s3:GetObject, s3:PutObject]`, ` Resource: !Sub arn:aws:s3:::\${StateBucket}/${STATE_KEY}`, ...(input.selfSchedule
|
|
541
|
+
lines.push(``, ` ForwarderRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: lambda.amazonaws.com }`, ` Action: sts:AssumeRole`, ` ManagedPolicyArns: [arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]`, ` Policies:`, ` - PolicyName: invoke-runtime`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: bedrock-agentcore:InvokeAgentRuntime`, ` Resource:`, ` - !GetAtt Runtime.AgentRuntimeArn`, ` - !Sub "\${Runtime.AgentRuntimeArn}/*"`, ` - Effect: Allow # mint the presigned URLs the container uses for its state snapshot`, ` Action: [s3:GetObject, s3:PutObject]`, ` Resource: !Sub arn:aws:s3:::\${StateBucket}/${STATE_KEY}`, ` # Without s3:ListBucket, S3 folds "key absent" into 403 (anti-enumeration), which is`, ` # indistinguishable from a broken signature — so the container's restore contract`, ` # (agentcore-state.ts: ONLY 404 means first deploy) would dead-end every first deploy.`, ` # Scoped to the snapshot prefix: this grants "may know whether the snapshot exists",`, ` # not a listing of the whole deployment bucket.`, ` - Effect: Allow`, ` Action: s3:ListBucket`, ` Resource: !Sub arn:aws:s3:::\${StateBucket}`, ` Condition:`, ` StringLike: { s3:prefix: state/* }`, ...(input.selfSchedule
|
|
542
542
|
? [
|
|
543
543
|
` - Effect: Allow # wake alarms: mirror pending wake-ups into one-shot schedules`,
|
|
544
544
|
` Action: [scheduler:CreateSchedule, scheduler:UpdateSchedule]`,
|
package/dist/deploy/preflight.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readdir, readFile } from "node:fs/promises";
|
|
13
13
|
import { basename, isAbsolute, join, relative, sep } from "node:path";
|
|
14
14
|
import ignore from "ignore";
|
|
15
|
+
import { classifyBind } from "../bind.js";
|
|
15
16
|
import { resolveAuthPath } from "../engines/pi/config.js";
|
|
16
17
|
import { resolveSecretsDir, resolveStateRoot } from "../paths.js";
|
|
17
18
|
import { inspectChannels } from "../engines/pi/channel.js";
|
|
@@ -341,6 +342,23 @@ export async function preflightDeploy(input) {
|
|
|
341
342
|
shipsGit,
|
|
342
343
|
};
|
|
343
344
|
const port = config.http?.port ?? 8787;
|
|
345
|
+
// `http.host` travels in the artifact (config is what deploy ships), and any non-wildcard value that
|
|
346
|
+
// is right on a laptop is wrong in a container: the wildcard bind is what makes the published port,
|
|
347
|
+
// the health check and webhook ingress reachable at all. `--bind` is the local-only knob; config is not.
|
|
348
|
+
const configBind = classifyBind(config.http?.host);
|
|
349
|
+
if (configBind !== "wildcard") {
|
|
350
|
+
const issue = `fastagent.config.ts sets http.host: "${config.http?.host}" — it travels into the image, where ` +
|
|
351
|
+
(configBind === "loopback"
|
|
352
|
+
? `nothing outside the container can reach the serve (published port, health check, webhooks).`
|
|
353
|
+
: `that address does not exist, so the container fails to bind at start.`) +
|
|
354
|
+
` Drop it and use \`--bind ${config.http?.host}\` locally instead.`;
|
|
355
|
+
// Same disposition as the model-travel issue: warn when producing artifacts (the operator may be
|
|
356
|
+
// deploying somewhere that fronts the port), gate `--run` — which would otherwise ship a box that
|
|
357
|
+
// answers nothing, or crash-loops on a bind that cannot resolve inside the container.
|
|
358
|
+
if (run)
|
|
359
|
+
return { ok: false, gate: issue };
|
|
360
|
+
messages.push({ level: "warn", text: issue });
|
|
361
|
+
}
|
|
344
362
|
// What the agent declared it needs on the box (fastagent.config deploy.secrets) — carried like channel
|
|
345
363
|
// secrets: listed in the runbook, set from the local env under --run, gated if a value is missing.
|
|
346
364
|
const extraSecrets = config.deploy?.secrets ?? [];
|
|
@@ -12,8 +12,11 @@ export interface FastagentConfig {
|
|
|
12
12
|
/** Extra custom tools, appended after pi defaults — never replaces them. `FastagentTool` = AgentTool
|
|
13
13
|
* plus the optional `deferred` marker (see defineTool). */
|
|
14
14
|
tools?: FastagentTool[];
|
|
15
|
+
/** `host` is the bind address: unset (or `0.0.0.0`) binds all interfaces — what containers need;
|
|
16
|
+
* `127.0.0.1` keeps the serve (including `/control/*`) off the LAN. Precedence: `--bind` > this. */
|
|
15
17
|
http?: {
|
|
16
18
|
port?: number;
|
|
19
|
+
host?: string;
|
|
17
20
|
};
|
|
18
21
|
/** Mount the built-in `wake` tool so the agent can schedule its OWN follow-up turns (self-scheduling).
|
|
19
22
|
* Off by default — self-scheduling is an autonomy capability, opt in when you want it. Only takes
|
|
@@ -24,8 +27,9 @@ export interface FastagentConfig {
|
|
|
24
27
|
* steer/abort/compact/set_model…) for remote consumers: a Web panel, a desktop app, `fastagent
|
|
25
28
|
* attach`. Default off (it is a remote-control surface). When on, `dev`/`start` generate a
|
|
26
29
|
* per-boot bearer token and write `<stateRoot>/control.json` for local discovery. The serve
|
|
27
|
-
* binds all interfaces, so the routes are LAN-reachable with the token as the only
|
|
28
|
-
*
|
|
30
|
+
* binds all interfaces by default, so the routes are LAN-reachable with the token as the only
|
|
31
|
+
* protection — bind loopback (`--bind 127.0.0.1`; not `http.host`, which travels into a deployed
|
|
32
|
+
* image), firewall the port, or wrap it for real exposure (design §14).
|
|
29
33
|
*/
|
|
30
34
|
sessionControl?: boolean;
|
|
31
35
|
/** Deploy-time declarations for what the agent needs on the box, so real agents don't hand-write a
|
|
@@ -21,6 +21,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
21
21
|
import { basename, join } from "node:path";
|
|
22
22
|
import { pathToFileURL } from "node:url";
|
|
23
23
|
import { THINKING_LEVELS } from "./harness.js";
|
|
24
|
+
import { isBindAddress } from "../../bind.js";
|
|
24
25
|
import { moduleLoadHint } from "../../loader.js";
|
|
25
26
|
import { AGENT_CONFIG_NAMES, resolveOverridePath, resolveSecretsDir } from "../../paths.js";
|
|
26
27
|
/** Identity function for typing and IDE completion (vite/next-style). */
|
|
@@ -116,13 +117,18 @@ export async function loadConfig(dir) {
|
|
|
116
117
|
throw new Error(`${path}: "http" must be an object`);
|
|
117
118
|
}
|
|
118
119
|
for (const key of Object.keys(c.http ?? {})) {
|
|
119
|
-
if (key !== "port") {
|
|
120
|
-
throw new Error(`${path}: unknown key "http.${key}" (valid keys: port)`);
|
|
120
|
+
if (key !== "port" && key !== "host") {
|
|
121
|
+
throw new Error(`${path}: unknown key "http.${key}" (valid keys: port, host)`);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
if (c.http?.port !== undefined && (typeof c.http.port !== "number" || !isValidPort(c.http.port))) {
|
|
124
125
|
throw new Error(`${path}: "http.port" must be an integer 0-65535`);
|
|
125
126
|
}
|
|
127
|
+
// Validated as strictly as http.port: an unbindable string ("banana") must fail HERE, not surface
|
|
128
|
+
// later as a topology diagnostic about "the interface you bound".
|
|
129
|
+
if (c.http?.host !== undefined && (typeof c.http.host !== "string" || !isBindAddress(c.http.host))) {
|
|
130
|
+
throw new Error(`${path}: "http.host" must be an IP address or "localhost" (e.g. "127.0.0.1", "0.0.0.0")`);
|
|
131
|
+
}
|
|
126
132
|
if (c.deploy !== undefined && (typeof c.deploy !== "object" || c.deploy === null)) {
|
|
127
133
|
throw new Error(`${path}: "deploy" must be an object`);
|
|
128
134
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ExecutionEnv, type Skill, type ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Models, Provider } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { Agent } from "../../agent.ts";
|
|
4
4
|
import { type FastagentConfig } from "./config.ts";
|
|
@@ -6,19 +6,20 @@ import { type LoadedDefinition } from "./definition.ts";
|
|
|
6
6
|
import { piHarnessFactory } from "./harness.ts";
|
|
7
7
|
import { type PiSessionStore } from "./sessions.ts";
|
|
8
8
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
9
|
-
import { type
|
|
9
|
+
import { type ToolCollision, type MountedTool } from "./tool.ts";
|
|
10
10
|
import { type Lease, type SessionObserver } from "./invoke.ts";
|
|
11
|
-
/** pi's core default toolset (read/bash/edit/write)
|
|
12
|
-
|
|
11
|
+
/** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
|
|
12
|
+
* turn as the harness tool context — hence no argument here. */
|
|
13
|
+
export declare function piDefaultTools(): MountedTool[];
|
|
13
14
|
/** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
|
|
14
|
-
export declare function resolveTools(config: FastagentConfig
|
|
15
|
+
export declare function resolveTools(config: FastagentConfig): MountedTool[];
|
|
15
16
|
/**
|
|
16
17
|
* The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
|
|
17
18
|
* existing win), plus the non-default tool names and collisions to report. One source for the
|
|
18
19
|
* dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
|
|
19
20
|
*/
|
|
20
|
-
export declare function resolveAgentTools(config: FastagentConfig, agentDir: string
|
|
21
|
-
tools:
|
|
21
|
+
export declare function resolveAgentTools(config: FastagentConfig, agentDir: string): Promise<{
|
|
22
|
+
tools: MountedTool[];
|
|
22
23
|
toolNames: string[];
|
|
23
24
|
/** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
|
|
24
25
|
* via the built-in `search_tools` loader. Surfaced so the operator can see deferral took effect. */
|
|
@@ -33,7 +34,7 @@ export declare function resolveAgentTools(config: FastagentConfig, agentDir: str
|
|
|
33
34
|
* `persona` (from persona.md) replaces the default identity line, keeping the tools list + guidelines.
|
|
34
35
|
*/
|
|
35
36
|
export declare function piBasePrompt(options?: {
|
|
36
|
-
tools?:
|
|
37
|
+
tools?: MountedTool[];
|
|
37
38
|
persona?: string;
|
|
38
39
|
}): string;
|
|
39
40
|
export interface AssembleSystemPromptOptions {
|
|
@@ -76,8 +77,10 @@ export interface CreatePiAgentOptions {
|
|
|
76
77
|
* or a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
|
|
77
78
|
*/
|
|
78
79
|
instructions?: string | (() => string);
|
|
79
|
-
/** `FastagentTool`
|
|
80
|
-
|
|
80
|
+
/** The tool set to mount. `FastagentTool` (AgentTool plus the optional `deferred` marker, see
|
|
81
|
+
* {@link DefineToolOptions}) widens into {@link MountedTool}, which additionally admits pi's default
|
|
82
|
+
* coding tools — they read the turn's ExecutionEnv as a fifth `execute` parameter. */
|
|
83
|
+
tools?: MountedTool[];
|
|
81
84
|
skills?: Skill[];
|
|
82
85
|
/**
|
|
83
86
|
* Extra providers registered on top of the built-ins — your own gateway / self-hosted endpoint /
|
|
@@ -93,8 +96,10 @@ export interface CreatePiAgentOptions {
|
|
|
93
96
|
authPath?: string;
|
|
94
97
|
/** Session persistence. Defaults to in-memory; inject jsonlSessionStore for restart-surviving continuity. */
|
|
95
98
|
sessions?: PiSessionStore;
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
99
|
+
/** Filesystem/process environment. Defaults to a local NodeExecutionEnv at `process.cwd()`, and its
|
|
100
|
+
* cwd is the agent's. The default coding tools (read/bash/edit/write) take it as the turn's tool
|
|
101
|
+
* context, so injecting a constrained one narrows where the agent reads, writes and shells. It does
|
|
102
|
+
* NOT constrain author-written `tools/`, which are code and can import anything. */
|
|
98
103
|
env?: ExecutionEnv;
|
|
99
104
|
/** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
|
|
100
105
|
lease?: Lease;
|
|
@@ -116,9 +121,9 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
116
121
|
/** Override the engine base prompt (segment ①). Defaults to piBasePrompt({ tools, persona }) using the
|
|
117
122
|
* live-read persona.md; pass base to fully opt out of persona.md. */
|
|
118
123
|
base?: string;
|
|
119
|
-
/** Override tools. Defaults to piDefaultTools (lock down with a custom list).
|
|
120
|
-
* AgentTool plus the optional `deferred` marker. */
|
|
121
|
-
tools?:
|
|
124
|
+
/** Override tools. Defaults to {@link piDefaultTools} (lock down with a custom list). An authored
|
|
125
|
+
* `FastagentTool[]` (AgentTool plus the optional `deferred` marker) widens into {@link MountedTool}. */
|
|
126
|
+
tools?: MountedTool[];
|
|
122
127
|
/**
|
|
123
128
|
* The agent's working directory: where the default tools operate AND whose ancestors are walked for
|
|
124
129
|
* ② project context (AGENTS.md). Defaults to `dir`. Set it to the enclosing repo so a coding agent
|
|
@@ -135,8 +140,11 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
135
140
|
*/
|
|
136
141
|
authPath?: string;
|
|
137
142
|
sessions?: PiSessionStore;
|
|
138
|
-
/**
|
|
139
|
-
*
|
|
143
|
+
/** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. At THIS rung it does more
|
|
144
|
+
* than root the default tools: persona.md and skills/ are read through it too. Two surfaces stay
|
|
145
|
+
* OUTSIDE it — ② project context (pi's loadProjectContextFiles uses node fs directly; see
|
|
146
|
+
* definition.ts) and author-written `tools/`, which are code and can import anything. Injecting an
|
|
147
|
+
* env narrows the blast radius rather than closing it. */
|
|
140
148
|
env?: ExecutionEnv;
|
|
141
149
|
lease?: Lease;
|
|
142
150
|
/** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* they come from the definition; the openers own model/tools — from config resolution).
|
|
12
12
|
*/
|
|
13
13
|
import { formatSkillsForSystemPrompt } from "@earendil-works/pi-agent-core";
|
|
14
|
+
import { createBashTool, createEditTool, createReadTool, createWriteTool, } from "@earendil-works/pi-agent-core";
|
|
14
15
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
15
|
-
import {
|
|
16
|
+
import { readImageProcessor } from "./read-image.js";
|
|
16
17
|
import { defaultAuthPath, resolveModel } from "./config.js";
|
|
17
18
|
import { resolveSecretsDir } from "../../paths.js";
|
|
18
19
|
import { loadAgentDefinition } from "./definition.js";
|
|
@@ -26,15 +27,36 @@ import { createPiAgentFromHarness, inProcessLease } from "./invoke.js";
|
|
|
26
27
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
27
28
|
//
|
|
28
29
|
// The full pi toolset is the default for fidelity: authors vibe in local pi with it, so serving with
|
|
29
|
-
// fewer tools is behavior drift.
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// fewer tools is behavior drift. Locking down for public exposure = passing a restricted `tools` list
|
|
31
|
+
// (a deployment posture).
|
|
32
|
+
//
|
|
33
|
+
// These are pi-agent-core's tools, which reach the filesystem and the shell through the
|
|
34
|
+
// {@link ExecutionEnv} the harness hands them per turn — NOT pi-coding-agent's, which are the same four
|
|
35
|
+
// tools wired to `node:fs` directly. Going through the env is the point, and the whole of it: it makes
|
|
36
|
+
// {@link CreatePiAgentOptions.env} the one seam a sandbox adapter has to implement, instead of a knob
|
|
37
|
+
// that governed everything except the tools that actually touch the machine. (It buys no decoupling
|
|
38
|
+
// from pi-coding-agent — definition.ts, models.ts and read-image.ts all import it regardless.)
|
|
39
|
+
//
|
|
40
|
+
// The swap holds only while the two behave alike, which they do NOT for free: core's `read` does
|
|
41
|
+
// nothing with images unless a processor is injected (read-image.ts), and both families are compared
|
|
42
|
+
// on every path in test/tools-parity.test.ts.
|
|
43
|
+
//
|
|
44
|
+
// `chat` is unaffected: it takes these NAMES only and lets pi's own runtime rebuild the tools it
|
|
45
|
+
// renders (see session-builder.ts).
|
|
46
|
+
/** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
|
|
47
|
+
* turn as the harness tool context — hence no argument here. */
|
|
48
|
+
export function piDefaultTools() {
|
|
49
|
+
// `read` needs its image pipeline INJECTED (core ships none); see read-image.ts for what is at stake.
|
|
50
|
+
return [
|
|
51
|
+
createReadTool({ imageProcessor: readImageProcessor }),
|
|
52
|
+
createBashTool(),
|
|
53
|
+
createEditTool(),
|
|
54
|
+
createWriteTool(),
|
|
55
|
+
];
|
|
34
56
|
}
|
|
35
57
|
/** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
|
|
36
|
-
export function resolveTools(config
|
|
37
|
-
const defaults = piDefaultTools(
|
|
58
|
+
export function resolveTools(config) {
|
|
59
|
+
const defaults = piDefaultTools();
|
|
38
60
|
return config.tools ? [...defaults, ...config.tools] : defaults;
|
|
39
61
|
}
|
|
40
62
|
/**
|
|
@@ -42,11 +64,12 @@ export function resolveTools(config, cwd) {
|
|
|
42
64
|
* existing win), plus the non-default tool names and collisions to report. One source for the
|
|
43
65
|
* dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
|
|
44
66
|
*/
|
|
45
|
-
export async function resolveAgentTools(config, agentDir
|
|
46
|
-
//
|
|
47
|
-
//
|
|
67
|
+
export async function resolveAgentTools(config, agentDir) {
|
|
68
|
+
// Discovered `tools/` come from `agentDir` (the agent's own surface); the default coding tools carry
|
|
69
|
+
// no root of their own — they operate through the ExecutionEnv handed to them per turn, whose cwd is
|
|
70
|
+
// the workspace.
|
|
48
71
|
const discovered = await loadTools(agentDir);
|
|
49
|
-
const merged = mergeDiscoveredTools(resolveTools(config
|
|
72
|
+
const merged = mergeDiscoveredTools(resolveTools(config), discovered.tools);
|
|
50
73
|
// The built-in `search_tools` loader mounts here — the one place the agent's full tool set is
|
|
51
74
|
// computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; an
|
|
52
75
|
// agent-defined search_tools wins).
|
|
@@ -59,7 +82,7 @@ export async function resolveAgentTools(config, agentDir, cwd = agentDir) {
|
|
|
59
82
|
// defaults, the builtin loader (like wake, a builtin gets its own report line, not an anonymous
|
|
60
83
|
// slot in the author's list — an author-DEFINED search_tools still shows), and deferred tools —
|
|
61
84
|
// each name lives in exactly ONE report slot, and deferred names live in `deferredToolNames`.
|
|
62
|
-
const defaultNames = new Set(piDefaultTools(
|
|
85
|
+
const defaultNames = new Set(piDefaultTools().map((t) => t.name));
|
|
63
86
|
const toolNames = tools
|
|
64
87
|
.filter((t) => !defaultNames.has(t.name) && !isDeferredTool(t) && !(builtinLoaderMounted && t.name === "search_tools"))
|
|
65
88
|
.map((t) => t.name);
|
|
@@ -216,7 +239,7 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
216
239
|
let reportedFindings = findingsSignature(definition);
|
|
217
240
|
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
218
241
|
// it; a caller's own search_tools wins).
|
|
219
|
-
const tools = withSearchTool(options.tools ?? piDefaultTools(
|
|
242
|
+
const tools = withSearchTool(options.tools ?? piDefaultTools());
|
|
220
243
|
const agent = buildPiAgent({
|
|
221
244
|
model: options.model,
|
|
222
245
|
thinkingLevel: options.thinkingLevel,
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* historical entries back into context via buildContext().
|
|
8
8
|
*/
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
|
-
import type {
|
|
10
|
+
import type { ExecutionEnv, ExecutionToolContext, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
11
11
|
import type { Model, Models } from "@earendil-works/pi-ai";
|
|
12
12
|
import type { PiSessionStore } from "./sessions.ts";
|
|
13
|
+
import { type MountedTool } from "./tool.ts";
|
|
13
14
|
/**
|
|
14
15
|
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
15
16
|
* tools a loader activated in that call. The DEDICATED record the resolve below reads: pi's own
|
|
@@ -19,18 +20,30 @@ import type { PiSessionStore } from "./sessions.ts";
|
|
|
19
20
|
* carry only what was actually discovered.
|
|
20
21
|
*/
|
|
21
22
|
export declare const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
23
|
+
/** The session a factory-built harness is bound to — the seam the activation bridge (invoke.ts) uses
|
|
24
|
+
* to write {@link TOOL_ACTIVATION_ENTRY} deltas (pi's harness keeps its session private). Absent for
|
|
25
|
+
* a harness built outside {@link piHarnessFactory}: activation still works in-turn there, but is not
|
|
26
|
+
* recorded — the factory owns persistence. */
|
|
27
|
+
type AnyHarness = AgentHarness<any>;
|
|
22
28
|
export type PiSession = Awaited<ReturnType<PiSessionStore["openOrCreate"]>>;
|
|
23
|
-
export declare function harnessSession(harness:
|
|
29
|
+
export declare function harnessSession(harness: AnyHarness): PiSession | undefined;
|
|
24
30
|
/**
|
|
25
31
|
* pi's Model with the API-shape generic erased — fastagent only passes models through to the
|
|
26
32
|
* harness, so the generic carries no information. One alias keeps the `any` auditable.
|
|
27
33
|
*/
|
|
28
34
|
export type AnyModel = Model<any>;
|
|
29
35
|
/** Builds a pi harness bound to the given session — called once per invoke. */
|
|
30
|
-
|
|
36
|
+
/** The harness fastagent builds: context-typed on {@link ExecutionToolContext}, because that is what
|
|
37
|
+
* pi's env-backed default tools read (pi 0.83). Custom tools are context-FREE and stay assignable — a
|
|
38
|
+
* four-parameter `execute` satisfies the five-parameter one, so `defineTool` is untouched by this. */
|
|
39
|
+
type PiHarness = AgentHarness<ExecutionToolContext>;
|
|
40
|
+
export type PiHarnessFactory = (session: string) => PiHarness | Promise<PiHarness>;
|
|
31
41
|
export interface PiHarnessFactoryOptions {
|
|
32
42
|
/** Session persistence. Continuity = same backing store + same session id. */
|
|
33
43
|
sessions: PiSessionStore;
|
|
44
|
+
/** Filesystem/process environment for the default coding tools. Handed to the harness as the TURN's
|
|
45
|
+
* tool context (pi 0.83), which is how read/bash/edit/write reach the machine at all — so this is the
|
|
46
|
+
* ONE seam a sandbox adapter implements, not a knob beside the tools that ignore it. */
|
|
34
47
|
env: ExecutionEnv;
|
|
35
48
|
/** Provider collection for all model requests; {@link model} must belong to it (same provider id). */
|
|
36
49
|
models: Models;
|
|
@@ -38,7 +51,7 @@ export interface PiHarnessFactoryOptions {
|
|
|
38
51
|
/** Reasoning effort for the model (pi's scale). Unset = fastagent's pinned default ("medium", pi
|
|
39
52
|
* TUI parity — see {@link DEFAULT_THINKING_LEVEL}); unsupported levels are clamped by pi per model. */
|
|
40
53
|
thinkingLevel?: ThinkingLevel;
|
|
41
|
-
tools?:
|
|
54
|
+
tools?: MountedTool[];
|
|
42
55
|
/**
|
|
43
56
|
* Final assembled prompt, or a SYNC factory re-evaluated per invoke (how L1 serves dynamic
|
|
44
57
|
* `instructions` + the skills listing). Distinct from {@link live}, which is the directory rung's
|
|
@@ -109,6 +122,7 @@ export declare function resolveHarnessOverrides(entries: OverrideEntryLike[], mo
|
|
|
109
122
|
model: AnyModel;
|
|
110
123
|
thinkingLevel: ThinkingLevel;
|
|
111
124
|
};
|
|
112
|
-
export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools:
|
|
125
|
+
export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: MountedTool[], sessionId: string): string[] | undefined;
|
|
113
126
|
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing → create. */
|
|
114
127
|
export declare function piHarnessFactory(options: PiHarnessFactoryOptions): PiHarnessFactory;
|
|
128
|
+
export {};
|
|
@@ -18,10 +18,6 @@ import { isDeferredTool } from "./tool.js";
|
|
|
18
18
|
* carry only what was actually discovered.
|
|
19
19
|
*/
|
|
20
20
|
export const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
21
|
-
/** The session a factory-built harness is bound to — the seam the activation bridge (invoke.ts) uses
|
|
22
|
-
* to write {@link TOOL_ACTIVATION_ENTRY} deltas (pi's harness keeps its session private). Absent for
|
|
23
|
-
* a harness built outside {@link piHarnessFactory}: activation still works in-turn there, but is not
|
|
24
|
-
* recorded — the factory owns persistence. */
|
|
25
21
|
const harnessSessions = new WeakMap();
|
|
26
22
|
export function harnessSession(harness) {
|
|
27
23
|
return harnessSessions.get(harness);
|
|
@@ -187,7 +183,9 @@ export function piHarnessFactory(options) {
|
|
|
187
183
|
// Session overrides (set_model / set_thinking) win over the assembly defaults — same entry walk.
|
|
188
184
|
const overrides = resolveHarnessOverrides(entries, options.models, { model: options.model, thinkingLevel: options.thinkingLevel ?? DEFAULT_THINKING_LEVEL }, sessionId);
|
|
189
185
|
const harness = new AgentHarness({
|
|
190
|
-
|
|
186
|
+
// Static, not a per-turn provider: the env is fixed for the agent's lifetime, and resolving a
|
|
187
|
+
// constant per turn would only add a promise to the turn's critical path.
|
|
188
|
+
toolContext: { env: options.env },
|
|
191
189
|
session,
|
|
192
190
|
models: options.models,
|
|
193
191
|
model: overrides.model,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
1
|
import type { Agent } from "../../agent.ts";
|
|
3
2
|
import { type FastagentConfig } from "./config.ts";
|
|
4
3
|
import type { SessionControl } from "../../session.ts";
|
|
@@ -7,6 +6,7 @@ import type { PiSessionReader, PiSessionStore } from "./sessions.ts";
|
|
|
7
6
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
8
7
|
import type { LoadedDefinition } from "./definition.ts";
|
|
9
8
|
import type { ToolCollision } from "./tool.ts";
|
|
9
|
+
import type { MountedTool } from "./tool.ts";
|
|
10
10
|
export interface CreatePiAgentFromDirOptions {
|
|
11
11
|
/** Model spec override (e.g. the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
|
|
12
12
|
model?: string;
|
|
@@ -66,7 +66,7 @@ export interface AgentAssembly {
|
|
|
66
66
|
/** Absolute credentials file (--auth-path/authPath option > FASTAGENT_AUTH_PATH > <agentDir>/.secrets/auth.json). */
|
|
67
67
|
authPath: string;
|
|
68
68
|
/** The full mounted tool surface (config.tools + discovered tools/, search_tools applied). */
|
|
69
|
-
tools:
|
|
69
|
+
tools: MountedTool[];
|
|
70
70
|
toolNames: string[];
|
|
71
71
|
deferredToolNames: string[];
|
|
72
72
|
toolCollisions: ToolCollision[];
|
package/dist/engines/pi/open.js
CHANGED
|
@@ -25,7 +25,7 @@ export async function resolveAgentAssembly(dir, options = {}) {
|
|
|
25
25
|
if (!modelSpec) {
|
|
26
26
|
throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
|
|
27
27
|
}
|
|
28
|
-
const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir
|
|
28
|
+
const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir);
|
|
29
29
|
// The state root: sessions/channel state/schedule state derive from it (FASTAGENT_STATE_DIR moves it
|
|
30
30
|
// in one knob — a container points it at its volume); the finer overrides below still win.
|
|
31
31
|
const stateRoot = resolveStateRoot(agentDir);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReadImageProcessor } from "@earendil-works/pi-agent-core";
|
|
2
|
+
/** The `read` tool's image processor. Matches pi-coding-agent's messages verbatim: they reach the model
|
|
3
|
+
* as tool output, so a reworded one is a different prompt, not a different implementation detail. */
|
|
4
|
+
export declare const readImageProcessor: ReadImageProcessor;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The image pipeline pi's `read` tool needs: normalize an unsupported format to PNG, resize below the
|
|
3
|
+
* inline limit, and hand back the hints that tell the model what it is looking at.
|
|
4
|
+
*
|
|
5
|
+
* pi-agent-core's `createReadTool` takes this as an INJECTED processor and does nothing without one —
|
|
6
|
+
* unlike pi-coding-agent's, which wires its private `processImage` internally. That function is not
|
|
7
|
+
* exported (nor reachable: the package's `exports` map has no deep paths), so this rebuilds it from the
|
|
8
|
+
* two halves that ARE public, `convertToPng` and `resizeImage`/`formatDimensionNote`.
|
|
9
|
+
*
|
|
10
|
+
* It is upstream logic restated, which is a real cost — without it `read` on a screenshot sends the raw
|
|
11
|
+
* bytes (measured: 7.48 MB of base64 where pi-coding-agent sends 3.48 MB, and no dimension note for the
|
|
12
|
+
* model's coordinate math), and a bmp is dropped entirely while the tool's own description still
|
|
13
|
+
* advertises it. test/tools-parity.test.ts compares this against pi-coding-agent's real `read` on both
|
|
14
|
+
* paths, so upstream changing the pipeline surfaces as a failing test rather than as drift.
|
|
15
|
+
*/
|
|
16
|
+
import { convertToPng, formatDimensionNote, resizeImage } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
/** Formats a provider takes inline as-is; everything else has to become a PNG first. */
|
|
18
|
+
const INLINE_MIME = {
|
|
19
|
+
"image/png": "image/png",
|
|
20
|
+
"image/jpeg": "image/jpeg",
|
|
21
|
+
"image/jpg": "image/jpeg",
|
|
22
|
+
"image/gif": "image/gif",
|
|
23
|
+
"image/webp": "image/webp",
|
|
24
|
+
};
|
|
25
|
+
/** The `read` tool's image processor. Matches pi-coding-agent's messages verbatim: they reach the model
|
|
26
|
+
* as tool output, so a reworded one is a different prompt, not a different implementation detail. */
|
|
27
|
+
export const readImageProcessor = async (bytes, mimeType, options) => {
|
|
28
|
+
const base = mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
|
|
29
|
+
const inline = INLINE_MIME[base];
|
|
30
|
+
let normalized;
|
|
31
|
+
if (inline) {
|
|
32
|
+
normalized = { bytes, mimeType: inline };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const png = await convertToPng(Buffer.from(bytes).toString("base64"), base);
|
|
36
|
+
if (!png)
|
|
37
|
+
return { ok: false, message: "[Image omitted: could not be converted to a supported inline image format.]" };
|
|
38
|
+
normalized = { bytes: Buffer.from(png.data, "base64"), mimeType: png.mimeType, convertedFrom: base };
|
|
39
|
+
}
|
|
40
|
+
const hints = [];
|
|
41
|
+
const converted = (to) => normalized.convertedFrom && normalized.convertedFrom !== to
|
|
42
|
+
? `[Image converted from ${normalized.convertedFrom} to ${to}.]`
|
|
43
|
+
: undefined;
|
|
44
|
+
if (!options.autoResizeImages) {
|
|
45
|
+
const hint = converted(normalized.mimeType);
|
|
46
|
+
if (hint)
|
|
47
|
+
hints.push(hint);
|
|
48
|
+
return { ok: true, data: Buffer.from(normalized.bytes).toString("base64"), mimeType: normalized.mimeType, hints };
|
|
49
|
+
}
|
|
50
|
+
const resized = await resizeImage(normalized.bytes, normalized.mimeType);
|
|
51
|
+
if (!resized)
|
|
52
|
+
return { ok: false, message: "[Image omitted: could not be resized below the inline image size limit.]" };
|
|
53
|
+
const hint = converted(resized.mimeType);
|
|
54
|
+
if (hint)
|
|
55
|
+
hints.push(hint);
|
|
56
|
+
// The scale factor the model needs to map coordinates back onto the original — dropping it is what
|
|
57
|
+
// makes a resized screenshot unusable for anything positional.
|
|
58
|
+
const note = formatDimensionNote(resized);
|
|
59
|
+
if (note)
|
|
60
|
+
hints.push(note);
|
|
61
|
+
return { ok: true, data: resized.data, mimeType: resized.mimeType, hints };
|
|
62
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type
|
|
2
|
-
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own.
|
|
3
|
-
|
|
1
|
+
import { type MountedTool } from "./tool.ts";
|
|
2
|
+
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own.
|
|
3
|
+
* Typed on the MOUNTED tool, not the authored one: it inspects names and deferral and never executes,
|
|
4
|
+
* so narrowing here would reject the very set it is handed (pi's defaults take the turn's context). */
|
|
5
|
+
export declare function withSearchTool(tools: MountedTool[]): MountedTool[];
|
|
4
6
|
/** Build the `search_tools` loader. Keyword search over the inactive tools' name+description.
|
|
5
7
|
*
|
|
6
8
|
* `executionMode: "sequential"` — pi turns any batch containing a sequential tool serial. Required for
|
|
7
9
|
* correct load-point attribution everywhere an OUTER active-set diff exists: pi wraps SDK customTools
|
|
8
10
|
* (the chat path) in a before/after diff, and two parallel loader calls would both snapshot the
|
|
9
11
|
* pre-activation set and get stamped with the same activation. Custom loader authors must set it too. */
|
|
10
|
-
export declare function makeSearchToolsTool():
|
|
12
|
+
export declare function makeSearchToolsTool(): MountedTool;
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { log } from "../../log.js";
|
|
10
10
|
import { defineTool, isDeferredTool, stripDeferredMarker } from "./tool.js";
|
|
11
|
-
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own.
|
|
11
|
+
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own.
|
|
12
|
+
* Typed on the MOUNTED tool, not the authored one: it inspects names and deferral and never executes,
|
|
13
|
+
* so narrowing here would reject the very set it is handed (pi's defaults take the turn's context). */
|
|
12
14
|
export function withSearchTool(tools) {
|
|
13
15
|
if (!tools.some(isDeferredTool))
|
|
14
16
|
return tools;
|
|
@@ -120,7 +120,7 @@ sessionManager) {
|
|
|
120
120
|
const env = new NodeExecutionEnv({ cwd });
|
|
121
121
|
const definition = await loadAgentDefinition(agentDir, { cwd, env });
|
|
122
122
|
reportDefinitionWarnings(definition.collisions, definition.diagnostics);
|
|
123
|
-
const defaultNames = piDefaultTools(
|
|
123
|
+
const defaultNames = piDefaultTools().map((t) => t.name);
|
|
124
124
|
const customTools = tools.filter((t) => !defaultNames.includes(t.name));
|
|
125
125
|
// Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
|
|
126
126
|
// it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
|
|
@@ -141,7 +141,12 @@ sessionManager) {
|
|
|
141
141
|
// session-lifecycle invariant as a normal out-of-turn call (fail visibly).
|
|
142
142
|
if (!bound)
|
|
143
143
|
throw new Error("tool executed before its session was built (lifecycle invariant broken)");
|
|
144
|
-
return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation },
|
|
144
|
+
return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation },
|
|
145
|
+
// pi's per-turn TOOL context (5th parameter) is read only by its default coding tools, and
|
|
146
|
+
// those are filtered out of `customTools` above; fastagent's own take theirs from
|
|
147
|
+
// `turnContext` (AsyncLocalStorage). So this env exists to satisfy the shape, and it is the
|
|
148
|
+
// chat cwd's — the same root a default tool would have got, had one reached here.
|
|
149
|
+
() => t.execute(id, params, signal, undefined, { env }));
|
|
145
150
|
},
|
|
146
151
|
}));
|
|
147
152
|
// base + instructions ONLY — pi appends the skill section and env (cwd) itself (including
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
1
|
+
import type { AgentHarnessTool, ExecutionToolContext, AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { type ModuleLoadFailure } from "../../loader.ts";
|
|
4
4
|
import { type ReadonlySessionManager, type ToolActivation } from "./tool-context.ts";
|
|
@@ -34,6 +34,14 @@ export interface DefineToolOptions<I extends z.ZodType> {
|
|
|
34
34
|
executionMode?: "sequential" | "parallel";
|
|
35
35
|
execute: (input: z.infer<I>, ctx: ToolContext) => unknown | Promise<unknown>;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A tool as MOUNTED: what the harness actually runs. Wider than the authored {@link AgentTool} on
|
|
39
|
+
* purpose — pi's default coding tools read the turn's tool context (its ExecutionEnv) as a fifth
|
|
40
|
+
* `execute` parameter, while fastagent's own tools take four and are assignable to it unchanged.
|
|
41
|
+
* Naming the wider type is what lets `defineTool` stay context-free for authors while both families
|
|
42
|
+
* live in one array; every helper that only inspects or reorders tools is typed on THIS.
|
|
43
|
+
*/
|
|
44
|
+
export type MountedTool = AgentHarnessTool<ExecutionToolContext>;
|
|
37
45
|
/** An AgentTool with fastagent's deferral marker — the type for raw tools handed to fastagent
|
|
38
46
|
* (`config.tools`, L1/L2 `tools`): plain `AgentTool` has no `deferred`, so an object literal with the
|
|
39
47
|
* marker would fail excess-property checking against upstream's type. `defineTool` produces it. */
|
|
@@ -42,10 +50,10 @@ export type FastagentTool = AgentTool & {
|
|
|
42
50
|
};
|
|
43
51
|
/** Read the {@link DefineToolOptions.deferred} marker off a mounted tool (extra property on the
|
|
44
52
|
* AgentTool object — pi ignores it). */
|
|
45
|
-
export declare function isDeferredTool(tool:
|
|
53
|
+
export declare function isDeferredTool(tool: MountedTool): boolean;
|
|
46
54
|
/** The same tool without the deferred marker — for a loader that must stay active (a deferred loader
|
|
47
55
|
* could never be activated and would strand every deferred tool). */
|
|
48
|
-
export declare function stripDeferredMarker(tool:
|
|
56
|
+
export declare function stripDeferredMarker(tool: MountedTool): MountedTool;
|
|
49
57
|
export declare function defineTool<I extends z.ZodType>(options: DefineToolOptions<I>): FastagentTool;
|
|
50
58
|
/** A discarded same-name tool (within `tools/`, or against an existing tool). Surfaced, never silent. */
|
|
51
59
|
export interface ToolCollision {
|
|
@@ -68,7 +76,7 @@ export declare function loadTools(dir: string): Promise<{
|
|
|
68
76
|
* Merge resolved tools (pi defaults + `config.tools`) with discovered `tools/`, deduped by name.
|
|
69
77
|
* Existing tools win; dropped discovered tools surface as collisions.
|
|
70
78
|
*/
|
|
71
|
-
export declare function mergeDiscoveredTools(existing:
|
|
72
|
-
tools:
|
|
79
|
+
export declare function mergeDiscoveredTools(existing: MountedTool[], discovered: AgentTool[]): {
|
|
80
|
+
tools: MountedTool[];
|
|
73
81
|
collisions: ToolCollision[];
|
|
74
82
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MountedTool } from "./tool.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Parse a delay to milliseconds: a number is SECONDS; a string MUST carry a unit — `"<n><s|m|h|d>"`
|
|
4
4
|
* ("30m", "2h", "1d"). Undefined for anything else, INCLUDING a bare numeric string like "120": one
|
|
@@ -12,6 +12,6 @@ export declare function parseDelayMs(input: string | number): number | undefined
|
|
|
12
12
|
* scheduler poller honors a wake-up) and only when the workspace hasn't defined its own `wake` (that
|
|
13
13
|
* wins, like any tool collision). The single place the mount decision + collision rule run.
|
|
14
14
|
*/
|
|
15
|
-
export declare function withWakeTool(tools:
|
|
15
|
+
export declare function withWakeTool(tools: MountedTool[], stateRoot: string, enabled: boolean): MountedTool[];
|
|
16
16
|
/** Build the `wake` tool bound to `stateRoot` (where wake-ups persist). */
|
|
17
|
-
export declare function makeWakeTool(stateRoot: string, now?: () => Date):
|
|
17
|
+
export declare function makeWakeTool(stateRoot: string, now?: () => Date): MountedTool;
|
package/dist/host/node.d.ts
CHANGED
|
@@ -48,9 +48,11 @@ export declare function router(routes: Routes): ChannelHandler;
|
|
|
48
48
|
* Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
|
|
49
49
|
* accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
|
|
50
50
|
* `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
|
|
51
|
+
* `host` is the bind address; unset means all interfaces (what containers need).
|
|
51
52
|
*/
|
|
52
53
|
export declare function serveNode(handler: ChannelHandler, options: {
|
|
53
54
|
port: number;
|
|
55
|
+
host?: string;
|
|
54
56
|
}): {
|
|
55
57
|
listening: Promise<number>;
|
|
56
58
|
close: () => Promise<void>;
|
package/dist/host/node.js
CHANGED
|
@@ -35,12 +35,13 @@ export function router(routes) {
|
|
|
35
35
|
* Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
|
|
36
36
|
* accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
|
|
37
37
|
* `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
|
|
38
|
+
* `host` is the bind address; unset means all interfaces (what containers need).
|
|
38
39
|
*/
|
|
39
40
|
export function serveNode(handler, options) {
|
|
40
41
|
const server = createServer(nodeListener(async (req) => handler(req)));
|
|
41
42
|
const listening = new Promise((resolve, reject) => {
|
|
42
43
|
server.once("error", reject); // a bind failure surfaces here, before "listening"
|
|
43
|
-
server.listen(options.port, () => {
|
|
44
|
+
server.listen({ port: options.port, host: options.host }, () => {
|
|
44
45
|
server.off("error", reject);
|
|
45
46
|
resolve(server.address().port);
|
|
46
47
|
});
|