@astrosheep/square 0.3.62 → 0.3.64
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/claude-plugin/.claude-plugin/plugin.json +3 -2
- package/claude-plugin/hooks/hooks.json +1 -22
- package/claude-plugin/hooks/register.js +119 -0
- package/claude-plugin/types.d.ts +7 -0
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity-feed.d.ts +1 -10
- package/dist/activity-feed.js +1 -27
- package/dist/activity.js +57 -25
- package/dist/artifact.js +12 -24
- package/dist/attention-presentation.js +1 -0
- package/dist/automatic-session.d.ts +3 -1
- package/dist/automatic-session.js +21 -34
- package/dist/boundary-presentation.js +4 -13
- package/dist/canonical-path.d.ts +11 -0
- package/dist/canonical-path.js +47 -0
- package/dist/catch-decisions.js +14 -10
- package/dist/catch-selection.d.ts +6 -0
- package/dist/catch-selection.js +14 -0
- package/dist/claude-delivery.d.ts +11 -0
- package/dist/claude-delivery.js +145 -0
- package/dist/claude-mod.d.ts +8 -0
- package/dist/claude-mod.js +164 -0
- package/dist/cli/context.d.ts +0 -1
- package/dist/cli/context.js +9 -21
- package/dist/cli/harness-command.d.ts +0 -1
- package/dist/cli/harness-command.js +0 -6
- package/dist/cli/observation-commands.d.ts +1 -1
- package/dist/cli/observation-commands.js +31 -58
- package/dist/cli/program.js +20 -18
- package/dist/cli/registry.js +2 -2
- package/dist/cli/square-commands.js +10 -6
- package/dist/codex-hook.js +9 -12
- package/dist/codex-queue.d.ts +2 -2
- package/dist/codex-queue.js +7 -5
- package/dist/decisions.d.ts +2 -6
- package/dist/decisions.js +16 -17
- package/dist/delivery-health.d.ts +1 -2
- package/dist/delivery-health.js +4 -5
- package/dist/delivery-operations.d.ts +2 -5
- package/dist/delivery-operations.js +77 -214
- package/dist/delivery.d.ts +25 -47
- package/dist/delivery.js +14 -46
- package/dist/diagnostic-redaction.js +7 -9
- package/dist/file-changes.d.ts +0 -1
- package/dist/file-changes.js +2 -19
- package/dist/harness-claude.js +8 -6
- package/dist/harness-codex.d.ts +0 -2
- package/dist/harness-codex.js +0 -2
- package/dist/harness-links.js +13 -17
- package/dist/harness.js +3 -7
- package/dist/help.js +4 -3
- package/dist/host-ledger-file-adapter.d.ts +17 -8
- package/dist/host-ledger-file-adapter.js +164 -80
- package/dist/host-ledger.d.ts +55 -50
- package/dist/inbox.d.ts +0 -3
- package/dist/inbox.js +13 -30
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/list.js +1 -4
- package/dist/mcp.js +18 -40
- package/dist/mention-parse.js +3 -9
- package/dist/model.d.ts +11 -2
- package/dist/model.js +1 -1
- package/dist/notifications.d.ts +3 -16
- package/dist/notifications.js +41 -122
- package/dist/opencode-delivery.d.ts +14 -0
- package/dist/opencode-delivery.js +171 -0
- package/dist/opencode.d.ts +39 -18
- package/dist/opencode.js +145 -40
- package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
- package/dist/packages/agent-delivery/src/claude-auth.js +70 -0
- package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
- package/dist/packages/agent-delivery/src/claude-native.js +65 -0
- package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
- package/dist/packages/agent-delivery/src/claude.js +66 -0
- package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
- package/dist/packages/agent-delivery/src/deadline.js +41 -0
- package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
- package/dist/packages/agent-delivery/src/errors.js +41 -0
- package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
- package/dist/packages/agent-delivery/src/guards.js +6 -0
- package/dist/packages/agent-delivery/src/index.d.ts +83 -0
- package/dist/packages/agent-delivery/src/index.js +110 -0
- package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
- package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
- package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
- package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
- package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
- package/dist/packages/agent-delivery/src/opencode.js +127 -0
- package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
- package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
- package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
- package/dist/packages/agent-delivery/src/paseo.js +106 -0
- package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
- package/dist/packages/agent-delivery/src/pi-client.js +106 -0
- package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
- package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
- package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
- package/dist/packages/agent-delivery/src/pi-send.js +4 -0
- package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
- package/dist/packages/agent-delivery/src/pi.js +209 -0
- package/dist/packages/agent-delivery/src/types.d.ts +120 -0
- package/dist/participant-host.js +12 -13
- package/dist/participant-identity.d.ts +21 -0
- package/dist/participant-identity.js +32 -11
- package/dist/paseo-delivery.d.ts +2 -2
- package/dist/paseo-delivery.js +35 -27
- package/dist/paseo-timeline.js +25 -21
- package/dist/paseo.d.ts +2 -2
- package/dist/paseo.js +1 -2
- package/dist/ports.d.ts +7 -20
- package/dist/presentation-operations.js +16 -4
- package/dist/presentation.d.ts +19 -10
- package/dist/presentation.js +99 -177
- package/dist/registry.d.ts +15 -66
- package/dist/registry.js +45 -123
- package/dist/routes.d.ts +1 -28
- package/dist/routes.js +22 -104
- package/dist/runtime.d.ts +3 -9
- package/dist/runtime.js +14 -67
- package/dist/search.d.ts +0 -2
- package/dist/search.js +3 -10
- package/dist/square-actions.js +48 -72
- package/dist/square-application.js +25 -23
- package/dist/square-core.d.ts +3 -0
- package/dist/square-core.js +22 -5
- package/dist/square-facade.d.ts +2 -0
- package/dist/square-file-adapter.js +4 -7
- package/dist/square-projections.d.ts +40 -45
- package/dist/square-projections.js +43 -73
- package/dist/square-state.js +1 -4
- package/dist/square-storage.d.ts +5 -8
- package/dist/square-storage.js +13 -36
- package/dist/square-wiring.d.ts +0 -5
- package/dist/square-wiring.js +3 -21
- package/dist/stream.js +4 -6
- package/dist/time.js +2 -7
- package/dist/views.d.ts +4 -16
- package/dist/views.js +12 -19
- package/dist/wake-attempts.d.ts +2 -56
- package/dist/wake-attempts.js +15 -110
- package/dist/wake-eligibility.d.ts +43 -0
- package/dist/wake-eligibility.js +106 -0
- package/dist/wake-evidence.d.ts +32 -9
- package/dist/wake-evidence.js +29 -24
- package/dist/wake-sink.d.ts +13 -9
- package/dist/wake-sink.js +35 -54
- package/dist/watch.js +13 -21
- package/extensions/square-pi.js +10 -5
- package/guides/architect.md +77 -77
- package/guides/brainstorm.md +197 -197
- package/guides/claude-native.md +101 -0
- package/guides/host-ledger.md +3 -1
- package/guides/participant.md +14 -7
- package/guides/pi-native-delivery.md +43 -0
- package/package.json +4 -3
- package/dist/claude-hook.d.ts +0 -10
- package/dist/claude-hook.js +0 -60
- package/dist/paseo-connection.d.ts +0 -15
- package/dist/paseo-connection.js +0 -141
- package/dist/presence.d.ts +0 -2
- package/dist/presence.js +0 -24
- package/dist/presented.d.ts +0 -2
- package/dist/presented.js +0 -10
- package/dist/state-cell.d.ts +0 -14
- package/dist/wake-port.d.ts +0 -23
- package/dist/wake-port.js +0 -27
- /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { DEFAULT_TIMEOUT_MS } from './deadline.js';
|
|
5
|
+
import { isIdentity } from './guards.js';
|
|
6
|
+
const DEFAULT_HOST = '127.0.0.1:6767';
|
|
7
|
+
/** The daemon writes a 32-byte base64url local credential; anything else is not one. */
|
|
8
|
+
const LOCAL_CREDENTIAL = /^[A-Za-z0-9_-]{43}$/;
|
|
9
|
+
let clientSequence = 0;
|
|
10
|
+
function expandHome(value) {
|
|
11
|
+
return value === '~' ? homedir() : value.startsWith('~/') ? join(homedir(), value.slice(2)) : value;
|
|
12
|
+
}
|
|
13
|
+
/** `~/` expansion plus the daemon's default home directory. */
|
|
14
|
+
export function paseoHome(explicit, env = process.env) {
|
|
15
|
+
const fromEnv = env.PASEO_HOME;
|
|
16
|
+
const value = isIdentity(explicit) ? explicit.trim() : typeof fromEnv === 'string' ? fromEnv.trim() : '';
|
|
17
|
+
return value === '' ? join(homedir(), '.paseo') : expandHome(value);
|
|
18
|
+
}
|
|
19
|
+
/** The daemon spellings the Paseo CLI accepts: IPC URIs, named pipes, socket paths, ports and `host:port`. */
|
|
20
|
+
function normalizedHost(raw) {
|
|
21
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
22
|
+
return undefined;
|
|
23
|
+
const value = raw.trim();
|
|
24
|
+
if (value.startsWith('unix://') || value.startsWith('pipe://') || value.startsWith('tcp://'))
|
|
25
|
+
return value;
|
|
26
|
+
if (value.startsWith('\\\\.\\pipe\\'))
|
|
27
|
+
return `pipe://${value}`;
|
|
28
|
+
if (value.startsWith('/') || value.startsWith('~/'))
|
|
29
|
+
return `unix://${expandHome(value)}`;
|
|
30
|
+
if (/^\d+$/.test(value))
|
|
31
|
+
return `127.0.0.1:${value}`;
|
|
32
|
+
return value.includes(':') ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
/** One resolution for every Paseo caller. Never starts or stops anything. */
|
|
35
|
+
export function resolvePaseoDaemon(endpoint, env = process.env, home = paseoHome(undefined, env)) {
|
|
36
|
+
// The caller's own endpoint wins; only an absent one falls through to `PASEO_HOST`
|
|
37
|
+
// and then to the daemon's own state files.
|
|
38
|
+
let host = endpoint !== undefined ? normalizedHost(endpoint) : normalizedHost(env.PASEO_HOST);
|
|
39
|
+
if (endpoint === undefined && host === undefined) {
|
|
40
|
+
for (const file of ['paseo.pid', 'config.json']) {
|
|
41
|
+
try {
|
|
42
|
+
const state = JSON.parse(readFileSync(join(home, file), 'utf8'));
|
|
43
|
+
host = normalizedHost(state.daemon?.listen ?? state.listen ?? state.sockPath);
|
|
44
|
+
if (host !== undefined)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
catch { /* absent or unreadable */ }
|
|
48
|
+
}
|
|
49
|
+
host ??= DEFAULT_HOST;
|
|
50
|
+
}
|
|
51
|
+
if (host === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (host.startsWith('unix://') || host.startsWith('pipe://')) {
|
|
54
|
+
const unix = host.startsWith('unix://');
|
|
55
|
+
if (unix && process.platform === 'win32')
|
|
56
|
+
return undefined;
|
|
57
|
+
const socketPath = expandHome(host.slice(unix ? 'unix://'.length : 'pipe://'.length).trim());
|
|
58
|
+
if (socketPath === '')
|
|
59
|
+
return undefined;
|
|
60
|
+
return { url: unix ? `ws+unix://${socketPath}:/ws` : 'ws://localhost/ws', socketPath };
|
|
61
|
+
}
|
|
62
|
+
if (host.startsWith('tcp://')) {
|
|
63
|
+
let uri;
|
|
64
|
+
try {
|
|
65
|
+
uri = new URL(host);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const name = uri.hostname.replace(/^\[|\]$/g, '');
|
|
71
|
+
if (name === '')
|
|
72
|
+
return undefined;
|
|
73
|
+
const address = `${name.includes(':') ? `[${name}]` : name}:${uri.port || '6767'}`;
|
|
74
|
+
const password = uri.searchParams.get('password');
|
|
75
|
+
return {
|
|
76
|
+
url: `${uri.searchParams.get('ssl') === 'true' ? 'wss' : 'ws'}://${address}/ws`,
|
|
77
|
+
...(password === null || password === '' ? {} : { password }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { url: `ws://${host.replace(/\/$/, '')}/ws` };
|
|
81
|
+
}
|
|
82
|
+
/** Explicit credential first, then the endpoint's own password, then the daemon's local credential, then `PASEO_PASSWORD`. */
|
|
83
|
+
export function daemonCredential(options, env = process.env, home = paseoHome(undefined, env)) {
|
|
84
|
+
if (isIdentity(options.authHeader))
|
|
85
|
+
return { authHeader: options.authHeader };
|
|
86
|
+
if (isIdentity(options.password))
|
|
87
|
+
return { password: options.password };
|
|
88
|
+
if (isIdentity(options.localCredential))
|
|
89
|
+
return { localCredential: () => options.localCredential };
|
|
90
|
+
if (isIdentity(options.endpointPassword))
|
|
91
|
+
return { password: options.endpointPassword };
|
|
92
|
+
try {
|
|
93
|
+
const token = readFileSync(join(home, 'local-credential'), 'utf8').trim();
|
|
94
|
+
if (LOCAL_CREDENTIAL.test(token))
|
|
95
|
+
return { localCredential: () => token };
|
|
96
|
+
}
|
|
97
|
+
catch { /* no readable local credential is not an error */ }
|
|
98
|
+
const password = env.PASEO_PASSWORD;
|
|
99
|
+
return isIdentity(password) ? { password: password.trim() } : {};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Open one daemon connection. The SDK and `ws` load only when a Paseo call actually
|
|
103
|
+
* runs, so importing this module never pulls an optional peer dependency.
|
|
104
|
+
*/
|
|
105
|
+
export async function openPaseoClient(target, credential = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
106
|
+
const sdk = await import('@getpaseo/client/internal/daemon-client');
|
|
107
|
+
const { default: WebSocket } = await import('ws');
|
|
108
|
+
return new sdk.DaemonClient({
|
|
109
|
+
url: target.url,
|
|
110
|
+
clientId: `agent-delivery-${process.pid}-${Date.now()}-${++clientSequence}`,
|
|
111
|
+
clientType: 'cli',
|
|
112
|
+
appVersion: 'agent-delivery',
|
|
113
|
+
...credential,
|
|
114
|
+
connectTimeoutMs: timeoutMs,
|
|
115
|
+
reconnect: { enabled: false },
|
|
116
|
+
// `ws` carries the daemon's auth headers and, for IPC targets, its socket path.
|
|
117
|
+
webSocketFactory: (url, options) => new WebSocket(url, options?.protocols, {
|
|
118
|
+
headers: options?.headers,
|
|
119
|
+
...(target.socketPath === undefined ? {} : { socketPath: target.socketPath }),
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PaseoDeliveryResult, PaseoSendTextOptions, PaseoTarget } from './types.js';
|
|
2
|
+
export interface PaseoConnectExistingOptions {
|
|
3
|
+
readonly harness: 'paseo';
|
|
4
|
+
readonly agentId: string;
|
|
5
|
+
readonly endpoint?: string;
|
|
6
|
+
readonly password?: string;
|
|
7
|
+
readonly authHeader?: string;
|
|
8
|
+
readonly localCredential?: string;
|
|
9
|
+
readonly paseoHome?: string;
|
|
10
|
+
readonly timeoutMs?: number;
|
|
11
|
+
readonly signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
/** Connect to an existing daemon and address one existing agent. Never starts or stops anything. */
|
|
14
|
+
export declare function connectPaseo(options: PaseoConnectExistingOptions): Promise<PaseoTarget>;
|
|
15
|
+
/**
|
|
16
|
+
* Every call owns its daemon connection: one for connect to prove the endpoint
|
|
17
|
+
* and credential, and one per send, closed again as soon as the daemon answers.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sendPaseoText(target: PaseoTarget, text: string, options?: PaseoSendTextOptions): Promise<PaseoDeliveryResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { deadline, validSignal, validTimeout } from './deadline.js';
|
|
3
|
+
import { ConnectionError } from './errors.js';
|
|
4
|
+
import { isIdentity, isRecord } from './guards.js';
|
|
5
|
+
import { daemonCredential, openPaseoClient, paseoHome, resolvePaseoDaemon, } from './paseo-native.js';
|
|
6
|
+
const connections = new WeakMap();
|
|
7
|
+
/** Connect to an existing daemon and address one existing agent. Never starts or stops anything. */
|
|
8
|
+
export async function connectPaseo(options) {
|
|
9
|
+
if (!isRecord(options) || options.harness !== 'paseo' || !isIdentity(options.agentId) ||
|
|
10
|
+
!validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
|
|
11
|
+
(options.endpoint !== undefined && !isIdentity(options.endpoint)) ||
|
|
12
|
+
(options.password !== undefined && !isIdentity(options.password)) ||
|
|
13
|
+
(options.authHeader !== undefined && !isIdentity(options.authHeader)) ||
|
|
14
|
+
(options.localCredential !== undefined && !isIdentity(options.localCredential)) ||
|
|
15
|
+
(options.paseoHome !== undefined && !isIdentity(options.paseoHome))) {
|
|
16
|
+
throw new ConnectionError('invalid_arguments');
|
|
17
|
+
}
|
|
18
|
+
const home = paseoHome(options.paseoHome);
|
|
19
|
+
const target = resolvePaseoDaemon(options.endpoint, process.env, home);
|
|
20
|
+
if (target === undefined)
|
|
21
|
+
throw new ConnectionError('invalid_arguments');
|
|
22
|
+
const credential = daemonCredential({ ...options, endpointPassword: target.password }, process.env, home);
|
|
23
|
+
const scope = deadline(options.timeoutMs, options.signal);
|
|
24
|
+
let client;
|
|
25
|
+
try {
|
|
26
|
+
client = await openPaseoClient(target, credential, options.timeoutMs);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new ConnectionError('service_unavailable');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
scope.check();
|
|
33
|
+
await scope.wait(client.connect());
|
|
34
|
+
scope.check();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (scope.code !== undefined)
|
|
38
|
+
throw new ConnectionError(scope.code);
|
|
39
|
+
if (error instanceof Error && error.name === 'DaemonAuthenticationError')
|
|
40
|
+
throw new ConnectionError('authentication_failed');
|
|
41
|
+
throw new ConnectionError('service_unavailable');
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
scope.close();
|
|
45
|
+
// A connected client owns a socket and a heartbeat timer. Nothing outlives
|
|
46
|
+
// this call, so a caller's process is free to exit the moment it is done.
|
|
47
|
+
await client.close().catch(() => { });
|
|
48
|
+
}
|
|
49
|
+
const addressed = Object.freeze({ harness: 'paseo', agentId: options.agentId });
|
|
50
|
+
connections.set(addressed, { target, credential });
|
|
51
|
+
return addressed;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Every call owns its daemon connection: one for connect to prove the endpoint
|
|
55
|
+
* and credential, and one per send, closed again as soon as the daemon answers.
|
|
56
|
+
*/
|
|
57
|
+
export async function sendPaseoText(target, text, options = {}) {
|
|
58
|
+
const connection = connections.get(target);
|
|
59
|
+
if (connection === undefined || typeof text !== 'string' || text.length === 0 ||
|
|
60
|
+
!validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
|
|
61
|
+
(options.inputId !== undefined && !isIdentity(options.inputId)) ||
|
|
62
|
+
(options.delivery !== undefined && options.delivery !== 'steer')) {
|
|
63
|
+
throw new TypeError('Invalid sendText arguments.');
|
|
64
|
+
}
|
|
65
|
+
const inputId = options.inputId ?? randomUUID();
|
|
66
|
+
const attempt = { harness: 'paseo', agentId: target.agentId, inputId };
|
|
67
|
+
const scope = deadline(options.timeoutMs, options.signal);
|
|
68
|
+
const client = await openPaseoClient(connection.target, connection.credential, options.timeoutMs);
|
|
69
|
+
// The connection is the uncertainty boundary: before it, nothing was written.
|
|
70
|
+
let connected = false;
|
|
71
|
+
try {
|
|
72
|
+
scope.check();
|
|
73
|
+
await scope.wait(client.connect());
|
|
74
|
+
connected = true;
|
|
75
|
+
scope.check();
|
|
76
|
+
await scope.wait(client.sendAgentMessage(target.agentId, text, { messageId: inputId, activeTurnBehavior: 'steer' }));
|
|
77
|
+
return { ...attempt, state: 'admitted' };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (scope.code !== undefined) {
|
|
81
|
+
return { ...attempt, state: connected ? 'unknown' : 'unavailable', code: scope.code };
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof Error && error.name === 'DaemonAuthenticationError') {
|
|
84
|
+
return { ...attempt, state: 'unavailable', code: 'authentication_failed' };
|
|
85
|
+
}
|
|
86
|
+
// Only a connection that completed can carry the daemon's own answer.
|
|
87
|
+
if (!connected)
|
|
88
|
+
return { ...attempt, state: 'unavailable', code: 'transport' };
|
|
89
|
+
if (error instanceof Error) {
|
|
90
|
+
// A receipt still `pending` means an earlier attempt may have landed.
|
|
91
|
+
if (error.message.includes('agent_request_outcome_unknown')) {
|
|
92
|
+
return { ...attempt, state: 'unknown', code: 'transport' };
|
|
93
|
+
}
|
|
94
|
+
if (error.name === 'Error') {
|
|
95
|
+
const refusal = error.message.includes('agent_request_key_conflict') ? 'key_conflict'
|
|
96
|
+
: /not found/i.test(error.message) ? 'not_found' : 'refused';
|
|
97
|
+
return { ...attempt, state: 'rejected', code: refusal };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { ...attempt, state: 'unknown', code: 'transport' };
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
scope.close();
|
|
104
|
+
await client.close().catch(() => { });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PiDeliveryResult, PiSendTextOptions, PiTarget } from './types.js';
|
|
2
|
+
export interface PiConnectExistingOptions {
|
|
3
|
+
readonly harness: 'pi';
|
|
4
|
+
readonly sessionId: string;
|
|
5
|
+
readonly endpoint: string;
|
|
6
|
+
readonly timeoutMs?: number;
|
|
7
|
+
readonly signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
export declare function connectPi(options: PiConnectExistingOptions): Promise<PiTarget>;
|
|
10
|
+
export declare function sendPiText(target: PiTarget, text: string, options?: PiSendTextOptions): Promise<PiDeliveryResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { deadline, validSignal } from './deadline.js';
|
|
3
|
+
import { ConnectionError } from './errors.js';
|
|
4
|
+
import { connectLocal, LocalEndpointError, parseLocalEndpoint } from './local-endpoint.js';
|
|
5
|
+
import { MAX_TEXT_BYTES, VERSION, frame, identity, readFrame, record, waitValid } from './pi-protocol.js';
|
|
6
|
+
const endpoints = new WeakMap();
|
|
7
|
+
const timeoutValid = (value) => value === undefined || waitValid(value);
|
|
8
|
+
/** One socket/one request. No automatic retry. Write attempt is the uncertainty boundary. */
|
|
9
|
+
async function request(endpoint, body, timeoutMs, signal) {
|
|
10
|
+
const scope = deadline(timeoutMs, signal);
|
|
11
|
+
let dispatched = false;
|
|
12
|
+
let socket;
|
|
13
|
+
let detach;
|
|
14
|
+
try {
|
|
15
|
+
scope.check();
|
|
16
|
+
const bytes = frame(body);
|
|
17
|
+
socket = await connectLocal(endpoint, { signal: scope.signal, timeoutMs });
|
|
18
|
+
scope.check();
|
|
19
|
+
// Conservative: an attempted write can have reached the native receiver even on later error.
|
|
20
|
+
dispatched = true;
|
|
21
|
+
const response = await scope.wait(new Promise((resolve, reject) => {
|
|
22
|
+
socket.on('error', () => reject(new Error()));
|
|
23
|
+
socket.on('close', () => reject(new Error()));
|
|
24
|
+
detach = readFrame(socket, resolve, () => reject(new ConnectionError('invalid_response')));
|
|
25
|
+
socket.write(bytes, (error) => { if (error)
|
|
26
|
+
reject(new Error()); });
|
|
27
|
+
}));
|
|
28
|
+
return { response, dispatched };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const local = error instanceof LocalEndpointError ? error : undefined;
|
|
32
|
+
// A failed local connect is Pi's transport failure: nothing reached the receiver.
|
|
33
|
+
const localCode = local === undefined || local.code === 'unavailable' ? undefined : local.code;
|
|
34
|
+
const code = scope.code ?? localCode ??
|
|
35
|
+
(error instanceof ConnectionError ? 'invalid_response' : 'transport');
|
|
36
|
+
return { dispatched, code };
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
detach?.();
|
|
40
|
+
socket?.destroy();
|
|
41
|
+
scope.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function connectPi(options) {
|
|
45
|
+
if (!record(options) || options.harness !== 'pi' || !identity(options.sessionId) ||
|
|
46
|
+
typeof options.endpoint !== 'string' || !timeoutValid(options.timeoutMs) || !validSignal(options.signal)) {
|
|
47
|
+
throw new ConnectionError('invalid_arguments');
|
|
48
|
+
}
|
|
49
|
+
let endpoint;
|
|
50
|
+
try {
|
|
51
|
+
endpoint = parseLocalEndpoint(options.endpoint);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new ConnectionError('invalid_arguments');
|
|
55
|
+
}
|
|
56
|
+
if (process.platform !== 'darwin')
|
|
57
|
+
throw new ConnectionError('unsupported_platform');
|
|
58
|
+
if (endpoint.kind !== 'unix')
|
|
59
|
+
throw new ConnectionError('invalid_arguments');
|
|
60
|
+
const answer = await request(endpoint, { protocol: VERSION, harness: 'pi', op: 'hello', sessionId: options.sessionId }, options.timeoutMs, options.signal);
|
|
61
|
+
if (answer.code !== undefined)
|
|
62
|
+
throw new ConnectionError(answer.code === 'transport' ? 'service_unavailable' : answer.code);
|
|
63
|
+
const value = answer.response;
|
|
64
|
+
if (record(value) && value.protocol === VERSION && value.state === 'rejected' && value.code === 'wrong_session') {
|
|
65
|
+
throw new ConnectionError('session_not_found');
|
|
66
|
+
}
|
|
67
|
+
if (!record(value) || value.protocol !== VERSION || value.harness !== 'pi' || value.sessionId !== options.sessionId) {
|
|
68
|
+
throw new ConnectionError('invalid_response');
|
|
69
|
+
}
|
|
70
|
+
const target = Object.freeze({ harness: 'pi', sessionId: options.sessionId });
|
|
71
|
+
endpoints.set(target, endpoint);
|
|
72
|
+
return target;
|
|
73
|
+
}
|
|
74
|
+
export async function sendPiText(target, text, options = {}) {
|
|
75
|
+
if (!record(target) || !endpoints.has(target) || typeof text !== 'string' || text.length === 0 ||
|
|
76
|
+
Buffer.byteLength(text) > MAX_TEXT_BYTES || !record(options) || !timeoutValid(options.timeoutMs) ||
|
|
77
|
+
!validSignal(options.signal) || options.inputId !== undefined ||
|
|
78
|
+
(options.delivery !== undefined && options.delivery !== 'steer' && options.delivery !== 'queue')) {
|
|
79
|
+
throw new TypeError('Invalid sendText arguments.');
|
|
80
|
+
}
|
|
81
|
+
const inputId = randomUUID();
|
|
82
|
+
const delivery = options.delivery ?? 'steer';
|
|
83
|
+
const attempt = { harness: 'pi', sessionId: target.sessionId, inputId };
|
|
84
|
+
const body = { protocol: VERSION, harness: 'pi', op: 'send',
|
|
85
|
+
sessionId: target.sessionId, inputId, text, delivery, timeoutMs: options.timeoutMs ?? 5_000 };
|
|
86
|
+
frame(body); // Reject excessive JSON escaping before opening any socket.
|
|
87
|
+
const answer = await request(endpoints.get(target), body, options.timeoutMs, options.signal);
|
|
88
|
+
if (answer.code !== undefined) {
|
|
89
|
+
return { ...attempt, state: answer.dispatched ? 'unknown' : 'unavailable', code: answer.code };
|
|
90
|
+
}
|
|
91
|
+
const value = answer.response;
|
|
92
|
+
if (record(value) && value.protocol === VERSION) {
|
|
93
|
+
if (value.state === 'rejected' && ['wrong_session', 'invalid_request', 'duplicate_inflight_id'].includes(value.code)) {
|
|
94
|
+
return { ...attempt, state: 'rejected', code: value.code };
|
|
95
|
+
}
|
|
96
|
+
if (value.harness === 'pi' && value.sessionId === target.sessionId && value.inputId === inputId && value.delivery === delivery) {
|
|
97
|
+
if (value.state === 'observed' && value.evidence === 'message_end') {
|
|
98
|
+
return { ...attempt, state: 'observed', evidence: 'message_end', delivery };
|
|
99
|
+
}
|
|
100
|
+
if (value.state === 'unknown' && ['timeout', 'transport', 'session_replaced', 'native_call'].includes(value.code)) {
|
|
101
|
+
return { ...attempt, state: 'unknown', code: value.code };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { ...attempt, state: 'unknown', code: 'invalid_response' };
|
|
106
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
export declare const VERSION = "agent-delivery/pi/1";
|
|
3
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
4
|
+
export declare const MAX_TEXT_BYTES: number;
|
|
5
|
+
export declare const MAX_WAIT_MS = 30000;
|
|
6
|
+
export declare const FRAME_WAIT_MS = 5000;
|
|
7
|
+
export declare const MAX_CONNECTIONS = 64;
|
|
8
|
+
export declare const CUSTOM_TYPE = "agent-delivery";
|
|
9
|
+
export declare const record: (value: unknown) => value is Record<string, unknown>;
|
|
10
|
+
export declare const identity: (value: unknown) => value is string;
|
|
11
|
+
export declare const endpointValid: (value: unknown) => value is string;
|
|
12
|
+
export declare const waitValid: (value: unknown) => value is number;
|
|
13
|
+
/** One bounded UTF-8 JSON object terminated only by LF, never Unicode separators. */
|
|
14
|
+
export declare function readFrame(socket: Socket, receive: (value: unknown) => void, invalid: () => void): () => void;
|
|
15
|
+
export declare function frame(value: unknown): Buffer;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
export const VERSION = 'agent-delivery/pi/1';
|
|
3
|
+
export const MAX_FRAME_BYTES = 256 * 1024;
|
|
4
|
+
export const MAX_TEXT_BYTES = 128 * 1024;
|
|
5
|
+
export const MAX_WAIT_MS = 30_000;
|
|
6
|
+
export const FRAME_WAIT_MS = 5_000;
|
|
7
|
+
export const MAX_CONNECTIONS = 64;
|
|
8
|
+
export const CUSTOM_TYPE = 'agent-delivery';
|
|
9
|
+
export const record = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
export const identity = (value) => typeof value === 'string' && value.trim().length > 0 && value.length <= 256;
|
|
11
|
+
export const endpointValid = (value) => typeof value === 'string' && isAbsolute(value) && !value.includes('\0') && Buffer.byteLength(value) <= 103;
|
|
12
|
+
export const waitValid = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 && value <= MAX_WAIT_MS;
|
|
13
|
+
/** One bounded UTF-8 JSON object terminated only by LF, never Unicode separators. */
|
|
14
|
+
export function readFrame(socket, receive, invalid) {
|
|
15
|
+
let chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
const onData = (chunk) => {
|
|
18
|
+
size += chunk.length;
|
|
19
|
+
if (size > MAX_FRAME_BYTES) {
|
|
20
|
+
stop();
|
|
21
|
+
invalid();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const newline = chunk.indexOf(10);
|
|
25
|
+
chunks.push(newline < 0 ? chunk : chunk.subarray(0, newline));
|
|
26
|
+
if (newline < 0)
|
|
27
|
+
return;
|
|
28
|
+
stop();
|
|
29
|
+
try {
|
|
30
|
+
if (newline !== chunk.length - 1)
|
|
31
|
+
throw new Error();
|
|
32
|
+
const bytes = Buffer.concat(chunks);
|
|
33
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
34
|
+
receive(JSON.parse(text));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
invalid();
|
|
38
|
+
}
|
|
39
|
+
chunks = [];
|
|
40
|
+
};
|
|
41
|
+
const stop = () => socket.off('data', onData);
|
|
42
|
+
socket.on('data', onData);
|
|
43
|
+
return stop;
|
|
44
|
+
}
|
|
45
|
+
export function frame(value) {
|
|
46
|
+
const bytes = Buffer.from(JSON.stringify(value) + '\n');
|
|
47
|
+
if (bytes.length > MAX_FRAME_BYTES)
|
|
48
|
+
throw new TypeError('Pi request is too large.');
|
|
49
|
+
return bytes;
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** SDK-free structural subset of Pi's custom message and native options. */
|
|
2
|
+
export interface PiMessage<T = unknown> {
|
|
3
|
+
customType: string;
|
|
4
|
+
content: string | ({
|
|
5
|
+
type: 'text';
|
|
6
|
+
text: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'image';
|
|
9
|
+
data: string;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
})[];
|
|
12
|
+
display: boolean;
|
|
13
|
+
details?: T;
|
|
14
|
+
}
|
|
15
|
+
export interface PiSendOptions {
|
|
16
|
+
deliverAs?: 'steer' | 'followUp' | 'nextTurn';
|
|
17
|
+
triggerTurn?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface PiSender {
|
|
20
|
+
sendMessage<T = unknown>(message: PiMessage<T>, options?: PiSendOptions): void;
|
|
21
|
+
}
|
|
22
|
+
/** One stateless invocation. Void is not admission or observable async failure. */
|
|
23
|
+
export declare function sendPiMessage<T>(pi: PiSender, message: PiMessage<T>, options?: PiSendOptions): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type PiSender } from './pi-send.js';
|
|
2
|
+
export { sendPiMessage } from './pi-send.js';
|
|
3
|
+
export type { PiMessage, PiSender, PiSendOptions } from './pi-send.js';
|
|
4
|
+
interface PiContext {
|
|
5
|
+
sessionManager: {
|
|
6
|
+
getSessionId(): string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface PiReceiverAPI extends PiSender {
|
|
10
|
+
on(event: 'session_start', handler: (event: unknown, ctx: PiContext) => Promise<void>): unknown;
|
|
11
|
+
on(event: 'session_shutdown', handler: () => Promise<void>): unknown;
|
|
12
|
+
on(event: 'message_end', handler: (event: {
|
|
13
|
+
message: unknown;
|
|
14
|
+
}, ctx: PiContext) => void): unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface PiReceiverOptions {
|
|
17
|
+
readonly endpoint?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resources start only on session_start. Missing endpoint leaves the extension inert.
|
|
21
|
+
* SDK hosts that dispose without session_shutdown must explicitly close this receiver.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createPiReceiver(pi: PiReceiverAPI, options?: PiReceiverOptions): {
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
};
|