@agentic-kit/pi-host 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.
@@ -0,0 +1,199 @@
1
+ // The coding lane's tools: a workspace, as tools.
2
+ //
3
+ // The pi control-plane tools (`@agentic-kit/pi`) are the *schema* lane — they
4
+ // resolve a Constructive project from the cwd and edit a tenant's database. A
5
+ // coding run's cwd is a git clone, so those tools have no project to act on;
6
+ // what it needs instead is the file surface of the checkout. pi's own
7
+ // read/write/edit/bash tools live inside the pi coding agent's TUI runtime
8
+ // rather than as `AgentTool`s an embedder can hand to `Agent`, so the four here
9
+ // are that surface and nothing more.
10
+ //
11
+ // Every path is resolved inside the workspace: a model that asks for
12
+ // `../../etc/passwd` gets an error result rather than the file, because the
13
+ // clone is the boundary the Job was given.
14
+ import { execFile } from 'node:child_process';
15
+ import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
16
+ import path from 'node:path';
17
+ import { isInside } from './project-context';
18
+ export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
19
+ export const MAX_READ_BYTES = 200_000;
20
+ export class OutsideWorkspaceError extends Error {
21
+ requested;
22
+ cwd;
23
+ constructor(requested, cwd) {
24
+ super(`'${requested}' is outside the workspace (${cwd})`);
25
+ this.requested = requested;
26
+ this.cwd = cwd;
27
+ this.name = 'OutsideWorkspaceError';
28
+ }
29
+ }
30
+ function resolveInside(cwd, requested) {
31
+ const resolved = path.resolve(cwd, requested);
32
+ if (!isInside(cwd, resolved))
33
+ throw new OutsideWorkspaceError(requested, cwd);
34
+ return resolved;
35
+ }
36
+ function text(value) {
37
+ return { content: [{ type: 'text', text: value }] };
38
+ }
39
+ function failure(error) {
40
+ return text(`error: ${error instanceof Error ? error.message : String(error)}`);
41
+ }
42
+ async function attempt(run) {
43
+ try {
44
+ return await run();
45
+ }
46
+ catch (error) {
47
+ // A tool failure is the model's to recover from — a bad path or a failing
48
+ // command is an ordinary result it should read and retry, not a crashed
49
+ // run. Anything that must end the run (a gateway that cannot be reached,
50
+ // a thread that is gone) is raised outside a tool.
51
+ return failure(error);
52
+ }
53
+ }
54
+ function runCommand(command, options) {
55
+ return new Promise((resolve) => {
56
+ execFile('/bin/sh', ['-c', command], {
57
+ cwd: options.cwd,
58
+ timeout: options.timeoutMs,
59
+ maxBuffer: MAX_READ_BYTES,
60
+ ...(options.env ? { env: options.env } : {})
61
+ }, (error, stdout, stderr) => {
62
+ const code = error && typeof error.code === 'number'
63
+ ? error.code
64
+ : error
65
+ ? 1
66
+ : 0;
67
+ resolve({ stdout, stderr: stderr + (error && !stderr ? String(error.message) : ''), code });
68
+ });
69
+ });
70
+ }
71
+ /**
72
+ * Read, write, edit, list, run. Named the way pi names them so a persona's
73
+ * tool allowlist reads the same in either host.
74
+ */
75
+ export function createWorkspaceTools(options) {
76
+ const cwd = path.resolve(options.cwd);
77
+ const timeoutMs = options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
78
+ return [
79
+ {
80
+ name: 'read_file',
81
+ label: 'Read a file',
82
+ description: 'Read a UTF-8 file from the workspace, relative to its root.',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: { path: { type: 'string', description: 'Path relative to the workspace root' } },
86
+ required: ['path'],
87
+ additionalProperties: false
88
+ },
89
+ execute: (_id, params) => attempt(async () => {
90
+ const file = resolveInside(cwd, String(params.path));
91
+ const info = await stat(file);
92
+ if (info.size > MAX_READ_BYTES) {
93
+ return text(`error: ${params.path} is ${info.size} bytes — too large to read whole`);
94
+ }
95
+ return text(await readFile(file, 'utf-8'));
96
+ })
97
+ },
98
+ {
99
+ name: 'write_file',
100
+ label: 'Write a file',
101
+ description: 'Write a UTF-8 file in the workspace, creating parent directories. Replaces the whole file.',
102
+ parameters: {
103
+ type: 'object',
104
+ properties: {
105
+ path: { type: 'string', description: 'Path relative to the workspace root' },
106
+ content: { type: 'string', description: 'The file\'s new contents' }
107
+ },
108
+ required: ['path', 'content'],
109
+ additionalProperties: false
110
+ },
111
+ execute: (_id, params) => attempt(async () => {
112
+ const file = resolveInside(cwd, String(params.path));
113
+ await mkdir(path.dirname(file), { recursive: true });
114
+ await writeFile(file, String(params.content), 'utf-8');
115
+ return text(`wrote ${params.path}`);
116
+ })
117
+ },
118
+ {
119
+ name: 'edit_file',
120
+ label: 'Edit a file',
121
+ description: 'Replace an exact string in a file. The string must occur exactly once, so an ambiguous edit fails rather than guessing.',
122
+ parameters: {
123
+ type: 'object',
124
+ properties: {
125
+ path: { type: 'string', description: 'Path relative to the workspace root' },
126
+ old_string: { type: 'string', description: 'The text to replace' },
127
+ new_string: { type: 'string', description: 'What to replace it with' }
128
+ },
129
+ required: ['path', 'old_string', 'new_string'],
130
+ additionalProperties: false
131
+ },
132
+ execute: (_id, params) => attempt(async () => {
133
+ const file = resolveInside(cwd, String(params.path));
134
+ const before = await readFile(file, 'utf-8');
135
+ const target = String(params.old_string);
136
+ const occurrences = before.split(target).length - 1;
137
+ if (occurrences === 0)
138
+ return text(`error: ${params.path} does not contain that text`);
139
+ if (occurrences > 1) {
140
+ return text(`error: that text occurs ${occurrences} times in ${params.path} — include enough context to make it unique`);
141
+ }
142
+ await writeFile(file, before.replace(target, String(params.new_string)), 'utf-8');
143
+ return text(`edited ${params.path}`);
144
+ })
145
+ },
146
+ {
147
+ name: 'list_files',
148
+ label: 'List files',
149
+ description: 'List the entries of a directory in the workspace.',
150
+ parameters: {
151
+ type: 'object',
152
+ properties: {
153
+ path: { type: 'string', description: 'Directory relative to the workspace root' }
154
+ },
155
+ additionalProperties: false
156
+ },
157
+ execute: (_id, params) => attempt(async () => {
158
+ const dir = resolveInside(cwd, params.path === undefined ? '.' : String(params.path));
159
+ const entries = await readdir(dir, { withFileTypes: true });
160
+ const listed = entries
161
+ .filter((entry) => entry.name !== '.git')
162
+ .map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
163
+ .sort();
164
+ return text(listed.join('\n') || '(empty)');
165
+ })
166
+ },
167
+ {
168
+ name: 'run_command',
169
+ label: 'Run a command',
170
+ description: 'Run a shell command in the workspace root and answer with its output and exit status.',
171
+ parameters: {
172
+ type: 'object',
173
+ properties: { command: { type: 'string', description: 'The command line to run' } },
174
+ required: ['command'],
175
+ additionalProperties: false
176
+ },
177
+ execute: (_id, params) => attempt(async () => {
178
+ const result = await runCommand(String(params.command), {
179
+ cwd,
180
+ timeoutMs,
181
+ ...(options.env ? { env: options.env } : {})
182
+ });
183
+ const parts = [
184
+ result.stdout.trim() ? `stdout:\n${result.stdout.trim()}` : '',
185
+ result.stderr.trim() ? `stderr:\n${result.stderr.trim()}` : '',
186
+ `exit: ${result.code}`
187
+ ].filter(Boolean);
188
+ return text(parts.join('\n\n'));
189
+ })
190
+ }
191
+ ];
192
+ }
193
+ export const WORKSPACE_TOOL_NAMES = [
194
+ 'read_file',
195
+ 'write_file',
196
+ 'edit_file',
197
+ 'list_files',
198
+ 'run_command'
199
+ ];
package/events.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { AgentEvent } from '@agentic-kit/agent';
2
+ import type { TaskWriter, TodoItem, Transcript } from '@agentic-kit/agent-conversation';
3
+ /** Tool names whose arguments carry the agent's todo list. */
4
+ export declare const TODO_TOOL_NAMES: string[];
5
+ export interface TranscriptWriterOptions {
6
+ transcript: Transcript;
7
+ /** Absent when the tenant's agent module has no task surface for this run. */
8
+ tasks?: TaskWriter;
9
+ /** Tool names whose arguments carry a todo list. */
10
+ todoToolNames?: string[];
11
+ }
12
+ /**
13
+ * Translate a run's events into thread writes.
14
+ *
15
+ * Nothing here is best-effort: a failed write is kept and rethrown from
16
+ * `drain()`, because a transcript that silently lost the middle of a run is
17
+ * worse than a run that fails.
18
+ */
19
+ export declare class TranscriptWriter {
20
+ private readonly options;
21
+ private chain;
22
+ private readonly pending;
23
+ private readonly todoToolNames;
24
+ private failure;
25
+ constructor(options: TranscriptWriterOptions);
26
+ /** Subscribe this writer to an agent: `agent.subscribe(writer.handler)`. */
27
+ readonly handler: (event: AgentEvent) => void;
28
+ /** Wait for every write queued so far, and surface the first that failed. */
29
+ drain(): Promise<void>;
30
+ private enqueue;
31
+ private apply;
32
+ private syncTodos;
33
+ }
34
+ /** The prose of an assistant message; thinking and tool calls are not prose. */
35
+ export declare function assistantText(message: {
36
+ content: unknown;
37
+ }): string;
38
+ /** The text of a tool result, as the transcript records it. */
39
+ export declare function resultText(result: {
40
+ content: unknown;
41
+ }): string;
42
+ /**
43
+ * The todo list a todo tool was called with. Returns null when the arguments do
44
+ * not carry one; throws when they carry a malformed one, because a todo list the
45
+ * tasks table cannot represent is a bug rather than an absence.
46
+ */
47
+ export declare function parseTodos(args: Record<string, unknown>): TodoItem[] | null;
package/events.js ADDED
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ // `AgentEvent` → the thread.
3
+ //
4
+ // `@agentic-kit/agent` already emits everything a transcript needs, so this is a
5
+ // translation and nothing else: prose to a message, a tool call to a ToolPart
6
+ // rewritten in place as it runs, the agent's todo list to `agent_task` rows.
7
+ // Writes are serialized through one promise chain so the thread's order matches
8
+ // the run's order; `drain()` is how a composition root waits for the tail.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.TranscriptWriter = exports.TODO_TOOL_NAMES = void 0;
11
+ exports.assistantText = assistantText;
12
+ exports.resultText = resultText;
13
+ exports.parseTodos = parseTodos;
14
+ const agent_conversation_1 = require("@agentic-kit/agent-conversation");
15
+ const coerce_1 = require("@constructive-io/coerce");
16
+ /** The text of a content block, or '' when the block carries none. */
17
+ const blockText = (block) => (0, coerce_1.asString)((0, coerce_1.asRecord)(block)?.text) ?? '';
18
+ /** Tool names whose arguments carry the agent's todo list. */
19
+ exports.TODO_TOOL_NAMES = ['todo_write', 'update_plan', 'write_todos'];
20
+ /**
21
+ * Translate a run's events into thread writes.
22
+ *
23
+ * Nothing here is best-effort: a failed write is kept and rethrown from
24
+ * `drain()`, because a transcript that silently lost the middle of a run is
25
+ * worse than a run that fails.
26
+ */
27
+ class TranscriptWriter {
28
+ options;
29
+ chain = Promise.resolve();
30
+ pending = new Map();
31
+ todoToolNames;
32
+ failure;
33
+ constructor(options) {
34
+ this.options = options;
35
+ this.todoToolNames = new Set(options.todoToolNames ?? exports.TODO_TOOL_NAMES);
36
+ }
37
+ /** Subscribe this writer to an agent: `agent.subscribe(writer.handler)`. */
38
+ handler = (event) => {
39
+ this.enqueue(() => this.apply(event));
40
+ };
41
+ /** Wait for every write queued so far, and surface the first that failed. */
42
+ async drain() {
43
+ await this.chain;
44
+ if (this.failure !== undefined) {
45
+ const failure = this.failure;
46
+ this.failure = undefined;
47
+ throw failure;
48
+ }
49
+ }
50
+ enqueue(work) {
51
+ this.chain = this.chain.then(async () => {
52
+ if (this.failure !== undefined)
53
+ return;
54
+ try {
55
+ await work();
56
+ }
57
+ catch (error) {
58
+ this.failure = error;
59
+ }
60
+ });
61
+ }
62
+ async apply(event) {
63
+ switch (event.type) {
64
+ case 'message_end': {
65
+ if (event.message.role !== 'assistant')
66
+ return;
67
+ const text = assistantText(event.message);
68
+ if (text)
69
+ await this.options.transcript.appendText(text);
70
+ return;
71
+ }
72
+ case 'tool_execution_start': {
73
+ const part = (0, agent_conversation_1.toolPart)({
74
+ toolName: event.toolName,
75
+ toolCallId: event.toolCallId,
76
+ input: event.args
77
+ });
78
+ const message = await this.options.transcript.appendToolPart(part);
79
+ this.pending.set(event.toolCallId, { messageId: message.id, part });
80
+ await this.syncTodos(event.toolName, event.args);
81
+ return;
82
+ }
83
+ case 'tool_execution_end': {
84
+ const call = this.pending.get(event.toolCallId);
85
+ if (!call) {
86
+ throw new Error(`tool call ${event.toolCallId} (${event.toolName}) ended without having started`);
87
+ }
88
+ this.pending.delete(event.toolCallId);
89
+ const output = resultText(event.result);
90
+ const next = event.isError
91
+ ? (0, agent_conversation_1.failToolPart)(call.part, output || 'The tool failed without a message')
92
+ : (0, agent_conversation_1.completeToolPart)(call.part, output);
93
+ await this.options.transcript.updateToolPart(call.messageId, next);
94
+ return;
95
+ }
96
+ case 'tool_decision_pending': {
97
+ // The lane's tools declare no `decision` schema — approval is the
98
+ // harness gate, answered through the thread. An in-band decision would
99
+ // stall the run forever, so it fails loudly instead.
100
+ throw new Error(`tool ${event.toolName} (${event.toolCallId}) asked for an in-band decision, which this runner does not serve`);
101
+ }
102
+ default:
103
+ return;
104
+ }
105
+ }
106
+ async syncTodos(toolName, args) {
107
+ if (!this.options.tasks || !this.todoToolNames.has(toolName))
108
+ return;
109
+ const todos = parseTodos(args);
110
+ if (todos)
111
+ await this.options.tasks.sync(todos);
112
+ }
113
+ }
114
+ exports.TranscriptWriter = TranscriptWriter;
115
+ /** The prose of an assistant message; thinking and tool calls are not prose. */
116
+ function assistantText(message) {
117
+ const content = message.content;
118
+ const prose = (0, coerce_1.asString)(content);
119
+ if (prose)
120
+ return prose.trim();
121
+ if (!Array.isArray(content))
122
+ return '';
123
+ return content
124
+ .map((block) => ((0, coerce_1.asRecord)(block)?.type === 'text' ? blockText(block) : ''))
125
+ .join('')
126
+ .trim();
127
+ }
128
+ /** The text of a tool result, as the transcript records it. */
129
+ function resultText(result) {
130
+ const content = result.content;
131
+ const text = (0, coerce_1.asString)(content);
132
+ if (text)
133
+ return text;
134
+ if (!Array.isArray(content))
135
+ return '';
136
+ return content.map(blockText).join('').trim();
137
+ }
138
+ const TODO_STATUSES = ['pending', 'in_progress', 'completed', 'failed'];
139
+ /**
140
+ * The todo list a todo tool was called with. Returns null when the arguments do
141
+ * not carry one; throws when they carry a malformed one, because a todo list the
142
+ * tasks table cannot represent is a bug rather than an absence.
143
+ */
144
+ function parseTodos(args) {
145
+ const raw = args.todos ?? args.items ?? args.plan;
146
+ if (raw === undefined || raw === null)
147
+ return null;
148
+ if (!Array.isArray(raw))
149
+ throw new Error('todo tool called with a non-list todo argument');
150
+ return raw.map((entry, index) => {
151
+ const item = (0, coerce_1.asRecord)(entry);
152
+ if (!item)
153
+ throw new Error(`todo ${index} is not an object`);
154
+ const description = (0, coerce_1.asString)(item.description ?? item.content ?? item.title ?? item.step);
155
+ if (!description)
156
+ throw new Error(`todo ${index} carries no description`);
157
+ const status = (0, coerce_1.asOneOf)(item.status ?? 'pending', TODO_STATUSES);
158
+ if (!status) {
159
+ throw new Error(`todo ${index} has status "${String(item.status)}", which agent_task does not model`);
160
+ }
161
+ const error = (0, coerce_1.asString)(item.error);
162
+ return {
163
+ description: description.trim(),
164
+ status,
165
+ ...(error ? { error } : {})
166
+ };
167
+ });
168
+ }
package/gate.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { Inbox, ToolPart, Transcript } from '@agentic-kit/agent-conversation';
2
+ import type { GateHost } from '@agentic-kit/harness';
3
+ export interface ThreadGateHostOptions {
4
+ transcript: Transcript;
5
+ inbox: Inbox;
6
+ /** How long a pending approval waits before it is treated as declined. */
7
+ approvalTimeoutMs?: number;
8
+ /** Approval ids, as a value — a suite passes a counter. */
9
+ newApprovalId?: () => string;
10
+ /** Called when the human asked the run to stop while an approval was pending. */
11
+ onCancel?: (reason: string | undefined) => void;
12
+ /** Called with every tool part the gate writes, so a caller can trace the run. */
13
+ onToolPart?: (part: ToolPart) => void;
14
+ }
15
+ export declare const DEFAULT_APPROVAL_TIMEOUT_MS: number;
16
+ /**
17
+ * The harness's `GateHost` plus the drain its one synchronous method needs.
18
+ *
19
+ * `notifyToolSkipped` returns void, so its transcript write cannot be awaited
20
+ * where it is called. It is queued instead, and `drain()` — awaited by the
21
+ * composition root once the run ends — rethrows the first failure, so a write
22
+ * that fails still fails the Job rather than disappearing into a `catch`.
23
+ */
24
+ export type ThreadGateHost = GateHost & {
25
+ drain(): Promise<void>;
26
+ };
27
+ /**
28
+ * A `GateHost` whose UI is the thread.
29
+ *
30
+ * The pending call is one message whose single ToolPart is rewritten in place as
31
+ * the decision arrives, so the thread reads as a conversation and the state
32
+ * machine in `@agentic-kit/agent-conversation` is the only place the
33
+ * lifecycle is enforced.
34
+ */
35
+ export declare function createThreadGateHost(options: ThreadGateHostOptions): ThreadGateHost;
package/gate.js ADDED
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ // The GateHost the harness already defines, answered by the conversation.
3
+ //
4
+ // `@agentic-kit/harness` owns *what* is gated (`MUTATING_DB_TOOLS`), the prompt
5
+ // wording (`buildConfirmPrompt`) and the decline guard; a host only supplies the
6
+ // three capabilities in `GateHost`. Desktop answers `confirmTool` with a dialog.
7
+ // A Job answers it with a message: it writes the tool call into the thread at
8
+ // `approval-requested` and waits for the human to echo the part back at
9
+ // `approval-responded`. That is the same contract, resolved asynchronously —
10
+ // which is exactly why the runner reports `hasUI: true`. Reporting false would
11
+ // tell the gate no decision can ever be obtained and block every mutating tool.
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.DEFAULT_APPROVAL_TIMEOUT_MS = void 0;
14
+ exports.createThreadGateHost = createThreadGateHost;
15
+ const agent_conversation_1 = require("@agentic-kit/agent-conversation");
16
+ exports.DEFAULT_APPROVAL_TIMEOUT_MS = 15 * 60 * 1000;
17
+ /**
18
+ * A `GateHost` whose UI is the thread.
19
+ *
20
+ * The pending call is one message whose single ToolPart is rewritten in place as
21
+ * the decision arrives, so the thread reads as a conversation and the state
22
+ * machine in `@agentic-kit/agent-conversation` is the only place the
23
+ * lifecycle is enforced.
24
+ */
25
+ function createThreadGateHost(options) {
26
+ const { transcript, inbox, approvalTimeoutMs = exports.DEFAULT_APPROVAL_TIMEOUT_MS, newApprovalId = () => `approval-${Math.random().toString(36).slice(2, 10)}`, onCancel, onToolPart } = options;
27
+ const emit = (part) => {
28
+ onToolPart?.(part);
29
+ return part;
30
+ };
31
+ const confirmTool = async (toolCallId, title, message, preview) => {
32
+ const pending = (0, agent_conversation_1.toolPart)({
33
+ toolName: 'confirm',
34
+ toolCallId,
35
+ input: { title, message, ...(preview ? { preview } : {}) }
36
+ });
37
+ const requested = emit((0, agent_conversation_1.requestApproval)(pending, newApprovalId()));
38
+ const written = await transcript.appendToolPart(requested);
39
+ const outcome = await inbox.waitFor(agent_conversation_1.isApprovalEvent, { timeoutMs: approvalTimeoutMs });
40
+ if (outcome.event && outcome.event.toolCallId === toolCallId) {
41
+ // Two writes on a decline, on purpose: `approval-responded` is the human's
42
+ // answer and `output-denied` is the call's outcome, and the transcript is
43
+ // read as a history — collapsing them would lose who declined and why.
44
+ const responded = (0, agent_conversation_1.respondToApproval)(requested, {
45
+ approved: outcome.event.approved,
46
+ reason: outcome.event.reason
47
+ });
48
+ await transcript.updateToolPart(written.id, emit(responded));
49
+ if (!outcome.event.approved) {
50
+ await transcript.updateToolPart(written.id, emit((0, agent_conversation_1.denyToolPart)(responded, outcome.event.reason ?? 'Declined by the user')));
51
+ }
52
+ return outcome.event.approved;
53
+ }
54
+ if (outcome.event) {
55
+ // A decision arrived for a call this gate is not waiting on. The gate is
56
+ // serial — one pending confirm at a time — so this is a malformed client,
57
+ // not a race, and silently dropping it would strand the run.
58
+ throw new Error(`approval for tool call ${outcome.event.toolCallId} arrived while ${toolCallId} was pending`);
59
+ }
60
+ if (outcome.cancelled) {
61
+ onCancel?.(outcome.cancelled.reason);
62
+ await transcript.updateToolPart(written.id, emit((0, agent_conversation_1.denyToolPart)(requested, outcome.cancelled.reason ?? 'Cancelled by the user')));
63
+ return false;
64
+ }
65
+ await transcript.updateToolPart(written.id, emit((0, agent_conversation_1.failToolPart)(requested, `No decision within ${Math.round(approvalTimeoutMs / 1000)}s`)));
66
+ return false;
67
+ };
68
+ // Queued notices, serialized so the transcript keeps the order the gate saw,
69
+ // and their first failure, kept for `drain()` to rethrow.
70
+ let queue = Promise.resolve();
71
+ let failure;
72
+ return {
73
+ hasUI: true,
74
+ confirmTool,
75
+ notifyToolSkipped: (toolCallId) => {
76
+ queue = queue.then(async () => {
77
+ try {
78
+ await transcript.appendText(`Skipped a repeat of a declined tool call (${toolCallId}).`);
79
+ }
80
+ catch (error) {
81
+ failure ??= error;
82
+ }
83
+ });
84
+ },
85
+ drain: async () => {
86
+ await queue;
87
+ if (failure !== undefined) {
88
+ throw new Error(`failed to write a skipped-tool notice to the thread: ${failure instanceof Error ? failure.message : String(failure)}`, { cause: failure });
89
+ }
90
+ }
91
+ };
92
+ }
package/host.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { ToolsHost } from '@agentic-kit/pi/host';
2
+ export interface PiHostValues {
3
+ userId: string;
4
+ accessToken: string;
5
+ apiKey?: string;
6
+ apiEndpoint?: string;
7
+ modulesEndpoint?: string;
8
+ }
9
+ /**
10
+ * A `ToolsHost` built from values.
11
+ *
12
+ * The desktop host reads an Electron singleton; a Job has a token and two
13
+ * endpoints, and nothing else. It offers no preview token, no data-auth broker
14
+ * and no step-up, so the data-plane tools decline cleanly rather than half-work,
15
+ * and no `deliverSecret`, so `create_api_key` refuses to mint a secret that has
16
+ * nowhere safe to go.
17
+ */
18
+ export declare function createPiToolsHost(values: PiHostValues): ToolsHost;
package/host.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ // The `ToolsHost` a Job can honestly provide.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.createPiToolsHost = createPiToolsHost;
5
+ /**
6
+ * A `ToolsHost` built from values.
7
+ *
8
+ * The desktop host reads an Electron singleton; a Job has a token and two
9
+ * endpoints, and nothing else. It offers no preview token, no data-auth broker
10
+ * and no step-up, so the data-plane tools decline cleanly rather than half-work,
11
+ * and no `deliverSecret`, so `create_api_key` refuses to mint a secret that has
12
+ * nowhere safe to go.
13
+ */
14
+ function createPiToolsHost(values) {
15
+ if (!values.accessToken)
16
+ throw new Error('a pi tools host needs an access token');
17
+ return {
18
+ account: () => ({
19
+ userId: values.userId,
20
+ accessToken: values.accessToken,
21
+ ...(values.apiKey ? { apiKey: values.apiKey } : {})
22
+ }),
23
+ backendConfig: () => ({
24
+ ...(values.apiEndpoint ? { apiEndpoint: values.apiEndpoint } : {}),
25
+ ...(values.modulesEndpoint ? { modulesEndpoint: values.modulesEndpoint } : {})
26
+ }),
27
+ signInHint: 'This run was launched without a usable platform token.'
28
+ };
29
+ }
package/index.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type { TranscriptWriterOptions } from './events';
2
+ export { assistantText, parseTodos, resultText, TODO_TOOL_NAMES, TranscriptWriter } from './events';
3
+ export type { ThreadGateHost, ThreadGateHostOptions } from './gate';
4
+ export { createThreadGateHost, DEFAULT_APPROVAL_TIMEOUT_MS } from './gate';
5
+ export type { PiHostValues } from './host';
6
+ export { createPiToolsHost } from './host';
7
+ export type { MeteredIdentity, MeteredModel, MeteredModelOptions } from './model';
8
+ export { createMeteredModel, gatewayApiRoot, meteringHeaders } from './model';
9
+ export type { LoadPersonaInput, PersonaConfig, PersonaRow, PersonaSelection, SelectPersonaInput, SkillResource } from './persona';
10
+ export { loadPersona, loadPersonaSkills, PersonaModelUnresolvedError, PersonaNotFoundError, selectPersona, UnknownPersonaToolError } from './persona';
11
+ export type { ProjectContextRequest } from './project-context';
12
+ export { isInside, materializeProjectContext, ProjectContextInsideWorkTreeError } from './project-context';
13
+ export type { GatedToolset, GatedToolsetOptions } from './tools';
14
+ export { CLONE_GATE_DEPS, createGatedToolset } from './tools';
15
+ export type { WorkspaceToolsOptions } from './workspace-tools';
16
+ export { createWorkspaceTools, DEFAULT_COMMAND_TIMEOUT_MS, MAX_READ_BYTES, OutsideWorkspaceError, WORKSPACE_TOOL_NAMES } from './workspace-tools';
package/index.js ADDED
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ // @agentic-kit/pi-host — the host half of a headless agent run: the
3
+ // harness's `GateHost` answered by the conversation thread, persona selection,
4
+ // the gated toolset, and `AgentEvent` → transcript. Every dependency arrives as
5
+ // a value; nothing here knows what a `ctx` is.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.WORKSPACE_TOOL_NAMES = exports.OutsideWorkspaceError = exports.MAX_READ_BYTES = exports.DEFAULT_COMMAND_TIMEOUT_MS = exports.createWorkspaceTools = exports.createGatedToolset = exports.CLONE_GATE_DEPS = exports.ProjectContextInsideWorkTreeError = exports.materializeProjectContext = exports.isInside = exports.UnknownPersonaToolError = exports.selectPersona = exports.PersonaNotFoundError = exports.PersonaModelUnresolvedError = exports.loadPersonaSkills = exports.loadPersona = exports.meteringHeaders = exports.gatewayApiRoot = exports.createMeteredModel = exports.createPiToolsHost = exports.DEFAULT_APPROVAL_TIMEOUT_MS = exports.createThreadGateHost = exports.TranscriptWriter = exports.TODO_TOOL_NAMES = exports.resultText = exports.parseTodos = exports.assistantText = void 0;
8
+ var events_1 = require("./events");
9
+ Object.defineProperty(exports, "assistantText", { enumerable: true, get: function () { return events_1.assistantText; } });
10
+ Object.defineProperty(exports, "parseTodos", { enumerable: true, get: function () { return events_1.parseTodos; } });
11
+ Object.defineProperty(exports, "resultText", { enumerable: true, get: function () { return events_1.resultText; } });
12
+ Object.defineProperty(exports, "TODO_TOOL_NAMES", { enumerable: true, get: function () { return events_1.TODO_TOOL_NAMES; } });
13
+ Object.defineProperty(exports, "TranscriptWriter", { enumerable: true, get: function () { return events_1.TranscriptWriter; } });
14
+ var gate_1 = require("./gate");
15
+ Object.defineProperty(exports, "createThreadGateHost", { enumerable: true, get: function () { return gate_1.createThreadGateHost; } });
16
+ Object.defineProperty(exports, "DEFAULT_APPROVAL_TIMEOUT_MS", { enumerable: true, get: function () { return gate_1.DEFAULT_APPROVAL_TIMEOUT_MS; } });
17
+ var host_1 = require("./host");
18
+ Object.defineProperty(exports, "createPiToolsHost", { enumerable: true, get: function () { return host_1.createPiToolsHost; } });
19
+ var model_1 = require("./model");
20
+ Object.defineProperty(exports, "createMeteredModel", { enumerable: true, get: function () { return model_1.createMeteredModel; } });
21
+ Object.defineProperty(exports, "gatewayApiRoot", { enumerable: true, get: function () { return model_1.gatewayApiRoot; } });
22
+ Object.defineProperty(exports, "meteringHeaders", { enumerable: true, get: function () { return model_1.meteringHeaders; } });
23
+ var persona_1 = require("./persona");
24
+ Object.defineProperty(exports, "loadPersona", { enumerable: true, get: function () { return persona_1.loadPersona; } });
25
+ Object.defineProperty(exports, "loadPersonaSkills", { enumerable: true, get: function () { return persona_1.loadPersonaSkills; } });
26
+ Object.defineProperty(exports, "PersonaModelUnresolvedError", { enumerable: true, get: function () { return persona_1.PersonaModelUnresolvedError; } });
27
+ Object.defineProperty(exports, "PersonaNotFoundError", { enumerable: true, get: function () { return persona_1.PersonaNotFoundError; } });
28
+ Object.defineProperty(exports, "selectPersona", { enumerable: true, get: function () { return persona_1.selectPersona; } });
29
+ Object.defineProperty(exports, "UnknownPersonaToolError", { enumerable: true, get: function () { return persona_1.UnknownPersonaToolError; } });
30
+ var project_context_1 = require("./project-context");
31
+ Object.defineProperty(exports, "isInside", { enumerable: true, get: function () { return project_context_1.isInside; } });
32
+ Object.defineProperty(exports, "materializeProjectContext", { enumerable: true, get: function () { return project_context_1.materializeProjectContext; } });
33
+ Object.defineProperty(exports, "ProjectContextInsideWorkTreeError", { enumerable: true, get: function () { return project_context_1.ProjectContextInsideWorkTreeError; } });
34
+ var tools_1 = require("./tools");
35
+ Object.defineProperty(exports, "CLONE_GATE_DEPS", { enumerable: true, get: function () { return tools_1.CLONE_GATE_DEPS; } });
36
+ Object.defineProperty(exports, "createGatedToolset", { enumerable: true, get: function () { return tools_1.createGatedToolset; } });
37
+ var workspace_tools_1 = require("./workspace-tools");
38
+ Object.defineProperty(exports, "createWorkspaceTools", { enumerable: true, get: function () { return workspace_tools_1.createWorkspaceTools; } });
39
+ Object.defineProperty(exports, "DEFAULT_COMMAND_TIMEOUT_MS", { enumerable: true, get: function () { return workspace_tools_1.DEFAULT_COMMAND_TIMEOUT_MS; } });
40
+ Object.defineProperty(exports, "MAX_READ_BYTES", { enumerable: true, get: function () { return workspace_tools_1.MAX_READ_BYTES; } });
41
+ Object.defineProperty(exports, "OutsideWorkspaceError", { enumerable: true, get: function () { return workspace_tools_1.OutsideWorkspaceError; } });
42
+ Object.defineProperty(exports, "WORKSPACE_TOOL_NAMES", { enumerable: true, get: function () { return workspace_tools_1.WORKSPACE_TOOL_NAMES; } });