pi-subagents-j0k3r 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.releaserc.json +14 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/index.ts +531 -0
- package/package.json +73 -0
- package/scripts/verify-package-files.mjs +41 -0
- package/skills/subagents-configuration/SKILL.md +182 -0
- package/src/config.ts +262 -0
- package/src/debug.ts +38 -0
- package/src/history.ts +254 -0
- package/src/interaction-channel.ts +197 -0
- package/src/manager.ts +533 -0
- package/src/model-profiles-ui.ts +609 -0
- package/src/profile-resolver.ts +60 -0
- package/src/runner.ts +688 -0
- package/src/thread-view.ts +477 -0
- package/src/tools.ts +492 -0
- package/src/types.ts +234 -0
- package/src/ui.ts +399 -0
package/src/history.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { boundThreadSnapshot } from './thread-view.js';
|
|
6
|
+
import type { SubagentTask, SubagentThreadSnapshot } from './types.js';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
type Db = {
|
|
11
|
+
exec(sql: string): void;
|
|
12
|
+
prepare(sql: string): {
|
|
13
|
+
run(...args: unknown[]): unknown;
|
|
14
|
+
all(...args: unknown[]): unknown[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function resolveSubagentsHistoryHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
19
|
+
if (env.PI_SUBAGENTS_HISTORY_HOME) return path.resolve(env.PI_SUBAGENTS_HISTORY_HOME);
|
|
20
|
+
const xdg = env.XDG_DATA_HOME;
|
|
21
|
+
return xdg ? path.join(xdg, 'pi', 'subagents') : path.join(os.homedir(), '.local', 'share', 'pi', 'subagents');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveSubagentHistoryDbPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
25
|
+
if (env.PI_SUBAGENTS_HISTORY_DB_PATH) return path.resolve(env.PI_SUBAGENTS_HISTORY_DB_PATH);
|
|
26
|
+
return path.join(resolveSubagentsHistoryHome(env), 'subagents-history.sqlite');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function value(text: string | undefined): string | null { return text ?? null; }
|
|
30
|
+
function snapshotJson(snapshot: SubagentTask['thread_snapshot']): string | null {
|
|
31
|
+
const bounded = boundThreadSnapshot(snapshot);
|
|
32
|
+
return bounded ? JSON.stringify(bounded) : null;
|
|
33
|
+
}
|
|
34
|
+
function parseSnapshotJson(text: unknown): SubagentThreadSnapshot | undefined {
|
|
35
|
+
if (typeof text !== 'string' || !text.trim()) return undefined;
|
|
36
|
+
try {
|
|
37
|
+
return boundThreadSnapshot(JSON.parse(text));
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
type HistoryReadOptions = { includeSnapshots?: boolean };
|
|
43
|
+
|
|
44
|
+
function ensureColumn(db: Db, table: string, column: string, definition: string): void {
|
|
45
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: string }>;
|
|
46
|
+
if (!columns.some((row) => row.name === column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function configureHistoryDb(db: Db): void {
|
|
50
|
+
db.exec('PRAGMA busy_timeout = 2000');
|
|
51
|
+
try { db.exec('PRAGMA journal_mode = WAL'); } catch {}
|
|
52
|
+
try { db.exec('PRAGMA synchronous = NORMAL'); } catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class SubagentHistoryStore {
|
|
56
|
+
private dbs = new Map<string, Db>();
|
|
57
|
+
|
|
58
|
+
private db(_cwd: string): Db {
|
|
59
|
+
const file = resolveSubagentHistoryDbPath();
|
|
60
|
+
const existing = this.dbs.get(file);
|
|
61
|
+
if (existing) return existing;
|
|
62
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
63
|
+
try { fs.chmodSync(path.dirname(file), 0o700); } catch {}
|
|
64
|
+
const { DatabaseSync } = require('node:sqlite') as any;
|
|
65
|
+
const db = new DatabaseSync(file) as Db;
|
|
66
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
67
|
+
configureHistoryDb(db);
|
|
68
|
+
db.exec(`
|
|
69
|
+
CREATE TABLE IF NOT EXISTS subagent_tasks (
|
|
70
|
+
id TEXT PRIMARY KEY,
|
|
71
|
+
cwd TEXT NOT NULL,
|
|
72
|
+
agent TEXT NOT NULL,
|
|
73
|
+
mode TEXT NOT NULL,
|
|
74
|
+
status TEXT NOT NULL,
|
|
75
|
+
task TEXT NOT NULL,
|
|
76
|
+
context TEXT,
|
|
77
|
+
created_at TEXT NOT NULL,
|
|
78
|
+
session_id TEXT,
|
|
79
|
+
started_at TEXT,
|
|
80
|
+
ended_at TEXT,
|
|
81
|
+
last_activity_at TEXT,
|
|
82
|
+
last_activity TEXT,
|
|
83
|
+
output_preview TEXT,
|
|
84
|
+
prompt TEXT,
|
|
85
|
+
system_prompt TEXT,
|
|
86
|
+
transcript TEXT,
|
|
87
|
+
usage_input INTEGER,
|
|
88
|
+
usage_output INTEGER,
|
|
89
|
+
usage_cache_read INTEGER,
|
|
90
|
+
usage_cache_write INTEGER,
|
|
91
|
+
usage_cost REAL,
|
|
92
|
+
usage_context_tokens INTEGER,
|
|
93
|
+
usage_turns INTEGER,
|
|
94
|
+
model TEXT,
|
|
95
|
+
effort TEXT,
|
|
96
|
+
model_source TEXT,
|
|
97
|
+
effort_source TEXT,
|
|
98
|
+
fallback_used INTEGER,
|
|
99
|
+
error TEXT,
|
|
100
|
+
result TEXT,
|
|
101
|
+
thread_snapshot_json TEXT
|
|
102
|
+
);
|
|
103
|
+
CREATE TABLE IF NOT EXISTS subagent_events (
|
|
104
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
105
|
+
task_id TEXT NOT NULL,
|
|
106
|
+
cwd TEXT NOT NULL,
|
|
107
|
+
created_at TEXT NOT NULL,
|
|
108
|
+
status TEXT NOT NULL,
|
|
109
|
+
activity TEXT NOT NULL,
|
|
110
|
+
output_preview TEXT,
|
|
111
|
+
FOREIGN KEY(task_id) REFERENCES subagent_tasks(id)
|
|
112
|
+
);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_subagent_tasks_created ON subagent_tasks(created_at DESC);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_subagent_events_task ON subagent_events(task_id, created_at);
|
|
115
|
+
`);
|
|
116
|
+
ensureColumn(db, 'subagent_tasks', 'system_prompt', 'TEXT');
|
|
117
|
+
this.dbs.set(file, db);
|
|
118
|
+
return db;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
upsertTask(cwd: string, task: SubagentTask): void {
|
|
122
|
+
this.db(cwd).prepare(`
|
|
123
|
+
INSERT INTO subagent_tasks (
|
|
124
|
+
id, cwd, agent, mode, status, task, context, created_at, session_id, started_at, ended_at,
|
|
125
|
+
last_activity_at, last_activity, output_preview, prompt, system_prompt, transcript,
|
|
126
|
+
usage_input, usage_output, usage_cache_read, usage_cache_write, usage_cost, usage_context_tokens, usage_turns,
|
|
127
|
+
model, effort, model_source, effort_source, fallback_used, error, result, thread_snapshot_json
|
|
128
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
129
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
130
|
+
status=excluded.status,
|
|
131
|
+
session_id=excluded.session_id,
|
|
132
|
+
started_at=excluded.started_at,
|
|
133
|
+
ended_at=excluded.ended_at,
|
|
134
|
+
last_activity_at=excluded.last_activity_at,
|
|
135
|
+
last_activity=excluded.last_activity,
|
|
136
|
+
output_preview=excluded.output_preview,
|
|
137
|
+
prompt=excluded.prompt,
|
|
138
|
+
system_prompt=excluded.system_prompt,
|
|
139
|
+
transcript=excluded.transcript,
|
|
140
|
+
usage_input=excluded.usage_input,
|
|
141
|
+
usage_output=excluded.usage_output,
|
|
142
|
+
usage_cache_read=excluded.usage_cache_read,
|
|
143
|
+
usage_cache_write=excluded.usage_cache_write,
|
|
144
|
+
usage_cost=excluded.usage_cost,
|
|
145
|
+
usage_context_tokens=excluded.usage_context_tokens,
|
|
146
|
+
usage_turns=excluded.usage_turns,
|
|
147
|
+
model=excluded.model,
|
|
148
|
+
effort=excluded.effort,
|
|
149
|
+
model_source=excluded.model_source,
|
|
150
|
+
effort_source=excluded.effort_source,
|
|
151
|
+
fallback_used=excluded.fallback_used,
|
|
152
|
+
error=excluded.error,
|
|
153
|
+
result=excluded.result,
|
|
154
|
+
thread_snapshot_json=excluded.thread_snapshot_json
|
|
155
|
+
`).run(
|
|
156
|
+
task.id,
|
|
157
|
+
cwd,
|
|
158
|
+
task.agent,
|
|
159
|
+
task.mode,
|
|
160
|
+
task.status,
|
|
161
|
+
task.task,
|
|
162
|
+
value(task.context),
|
|
163
|
+
task.created_at,
|
|
164
|
+
value(task.session_id),
|
|
165
|
+
value(task.started_at),
|
|
166
|
+
value(task.ended_at),
|
|
167
|
+
value(task.last_activity_at),
|
|
168
|
+
value(task.last_activity),
|
|
169
|
+
value(task.output_preview),
|
|
170
|
+
value(task.prompt),
|
|
171
|
+
value(task.system_prompt),
|
|
172
|
+
value(task.transcript),
|
|
173
|
+
task.usage?.input ?? null,
|
|
174
|
+
task.usage?.output ?? null,
|
|
175
|
+
task.usage?.cacheRead ?? null,
|
|
176
|
+
task.usage?.cacheWrite ?? null,
|
|
177
|
+
task.usage?.cost ?? null,
|
|
178
|
+
task.usage?.contextTokens ?? null,
|
|
179
|
+
task.usage?.turns ?? null,
|
|
180
|
+
value(task.model),
|
|
181
|
+
value(task.effort),
|
|
182
|
+
value(task.model_source),
|
|
183
|
+
value(task.effort_source),
|
|
184
|
+
task.fallback_used === undefined ? null : task.fallback_used ? 1 : 0,
|
|
185
|
+
value(task.error),
|
|
186
|
+
value(task.result),
|
|
187
|
+
snapshotJson(task.thread_snapshot),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
addEvent(cwd: string, task: SubagentTask, activity: string): void {
|
|
192
|
+
this.db(cwd).prepare(`
|
|
193
|
+
INSERT INTO subagent_events (task_id, cwd, created_at, status, activity, output_preview)
|
|
194
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
195
|
+
`).run(task.id, cwd, task.last_activity_at ?? new Date().toISOString(), task.status, activity, value(task.output_preview));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getTask(cwd: string, id: string, options: HistoryReadOptions = {}): SubagentTask | undefined {
|
|
199
|
+
const rows = this.db(cwd).prepare(`
|
|
200
|
+
SELECT * FROM subagent_tasks WHERE cwd = ? AND id = ? LIMIT 1
|
|
201
|
+
`).all(cwd, id);
|
|
202
|
+
return rows.length ? rowToTask(rows[0], options) : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
listTasks(cwd: string, limit = 100, options: HistoryReadOptions = {}): SubagentTask[] {
|
|
206
|
+
return this.db(cwd).prepare(`
|
|
207
|
+
SELECT * FROM subagent_tasks WHERE cwd = ? ORDER BY created_at DESC LIMIT ?
|
|
208
|
+
`).all(cwd, limit).map((row) => rowToTask(row, options));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
listSessionTasks(cwd: string, sessionId: string, limit = 100, options: HistoryReadOptions = {}): SubagentTask[] {
|
|
212
|
+
return this.db(cwd).prepare(`
|
|
213
|
+
SELECT * FROM subagent_tasks WHERE cwd = ? AND session_id = ? ORDER BY created_at DESC LIMIT ?
|
|
214
|
+
`).all(cwd, sessionId, limit).map((row) => rowToTask(row, options));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function rowToTask(row: any, options: HistoryReadOptions = {}): SubagentTask {
|
|
219
|
+
return {
|
|
220
|
+
id: row.id,
|
|
221
|
+
agent: row.agent,
|
|
222
|
+
mode: row.mode,
|
|
223
|
+
status: row.status,
|
|
224
|
+
task: row.task,
|
|
225
|
+
context: row.context ?? undefined,
|
|
226
|
+
created_at: row.created_at,
|
|
227
|
+
session_id: row.session_id ?? undefined,
|
|
228
|
+
started_at: row.started_at ?? undefined,
|
|
229
|
+
ended_at: row.ended_at ?? undefined,
|
|
230
|
+
last_activity_at: row.last_activity_at ?? undefined,
|
|
231
|
+
last_activity: row.last_activity ?? undefined,
|
|
232
|
+
output_preview: row.output_preview ?? undefined,
|
|
233
|
+
prompt: row.prompt ?? undefined,
|
|
234
|
+
system_prompt: row.system_prompt ?? undefined,
|
|
235
|
+
transcript: row.transcript ?? undefined,
|
|
236
|
+
usage: row.usage_input === null && row.usage_output === null && row.usage_cache_read === null && row.usage_cache_write === null && row.usage_cost === null && row.usage_context_tokens === null && row.usage_turns === null ? undefined : {
|
|
237
|
+
input: row.usage_input ?? 0,
|
|
238
|
+
output: row.usage_output ?? 0,
|
|
239
|
+
cacheRead: row.usage_cache_read ?? 0,
|
|
240
|
+
cacheWrite: row.usage_cache_write ?? 0,
|
|
241
|
+
cost: row.usage_cost ?? 0,
|
|
242
|
+
contextTokens: row.usage_context_tokens ?? 0,
|
|
243
|
+
turns: row.usage_turns ?? 0,
|
|
244
|
+
},
|
|
245
|
+
model: row.model ?? undefined,
|
|
246
|
+
effort: row.effort ?? undefined,
|
|
247
|
+
model_source: row.model_source ?? undefined,
|
|
248
|
+
effort_source: row.effort_source ?? undefined,
|
|
249
|
+
fallback_used: row.fallback_used === null || row.fallback_used === undefined ? undefined : Boolean(row.fallback_used),
|
|
250
|
+
error: row.error ?? undefined,
|
|
251
|
+
result: row.result ?? undefined,
|
|
252
|
+
thread_snapshot: options.includeSnapshots === false ? undefined : parseSnapshotJson(row.thread_snapshot_json),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export type InteractionStatus = 'answered' | 'cancelled' | 'failed';
|
|
4
|
+
|
|
5
|
+
export type InteractionRequester = {
|
|
6
|
+
subagentId?: string;
|
|
7
|
+
subagentName?: string;
|
|
8
|
+
taskId?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type InteractionPrompt = {
|
|
14
|
+
title?: string;
|
|
15
|
+
message?: string;
|
|
16
|
+
choices?: string[];
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
safeTarget?: string;
|
|
20
|
+
safeCommandSummary?: string;
|
|
21
|
+
workspaceRoot?: string;
|
|
22
|
+
limitations?: string[];
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type InteractionResponseExpectation = {
|
|
27
|
+
expected?: 'boolean' | 'choice' | 'string' | 'json' | 'unknown';
|
|
28
|
+
required?: boolean;
|
|
29
|
+
instructions?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type SubagentInteractionRequest = {
|
|
34
|
+
type: 'interaction_required';
|
|
35
|
+
requestId: string;
|
|
36
|
+
kind: string;
|
|
37
|
+
origin?: string;
|
|
38
|
+
requester?: InteractionRequester;
|
|
39
|
+
reason?: string;
|
|
40
|
+
reasonCode?: string;
|
|
41
|
+
riskLevel?: string;
|
|
42
|
+
prompt?: InteractionPrompt;
|
|
43
|
+
payload?: unknown;
|
|
44
|
+
response?: InteractionResponseExpectation;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type SubagentInteractionResponse = {
|
|
49
|
+
type: 'interaction_response';
|
|
50
|
+
requestId: string;
|
|
51
|
+
status: InteractionStatus;
|
|
52
|
+
value?: unknown;
|
|
53
|
+
error?: string;
|
|
54
|
+
responder?: 'parent' | string;
|
|
55
|
+
answeredAt?: string;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export interface PublishedInteractionRequest {
|
|
60
|
+
handle: string;
|
|
61
|
+
payload: SubagentInteractionRequest;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
consumed?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const INTERACTION_CHANNEL_KEY = Symbol.for('pi.subagents.interactionChannel');
|
|
67
|
+
const INTERACTION_RESPONSE_CHANNEL_KEY = Symbol.for('pi.subagents.interactionResponses');
|
|
68
|
+
|
|
69
|
+
function requestRegistry(): Map<string, PublishedInteractionRequest> {
|
|
70
|
+
const holder = globalThis as Record<symbol, unknown>;
|
|
71
|
+
const existing = holder[INTERACTION_CHANNEL_KEY];
|
|
72
|
+
if (existing instanceof Map) return existing as Map<string, PublishedInteractionRequest>;
|
|
73
|
+
const registry = new Map<string, PublishedInteractionRequest>();
|
|
74
|
+
holder[INTERACTION_CHANNEL_KEY] = registry;
|
|
75
|
+
return registry;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function responseRegistry(): Map<string, SubagentInteractionResponse> {
|
|
79
|
+
const holder = globalThis as Record<symbol, unknown>;
|
|
80
|
+
const existing = holder[INTERACTION_RESPONSE_CHANNEL_KEY];
|
|
81
|
+
if (existing instanceof Map) return existing as Map<string, SubagentInteractionResponse>;
|
|
82
|
+
const registry = new Map<string, SubagentInteractionResponse>();
|
|
83
|
+
holder[INTERACTION_RESPONSE_CHANNEL_KEY] = registry;
|
|
84
|
+
return registry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
88
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function requestIdFrom(value: Record<string, unknown>): string | undefined {
|
|
92
|
+
const requestId = value.requestId ?? value.request_id;
|
|
93
|
+
return typeof requestId === 'string' && requestId.length > 0 ? requestId : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function promptFrom(value: Record<string, unknown>): InteractionPrompt | undefined {
|
|
97
|
+
return isRecord(value.prompt) ? value.prompt as InteractionPrompt : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findInteractionRequest(value: unknown, seen: Set<unknown>): SubagentInteractionRequest | undefined {
|
|
101
|
+
if (!isRecord(value) || seen.has(value)) return undefined;
|
|
102
|
+
seen.add(value);
|
|
103
|
+
|
|
104
|
+
const directRequestId = requestIdFrom(value);
|
|
105
|
+
if (value.type === 'interaction_required' && directRequestId) {
|
|
106
|
+
return {
|
|
107
|
+
...value,
|
|
108
|
+
type: 'interaction_required',
|
|
109
|
+
requestId: directRequestId,
|
|
110
|
+
kind: typeof value.kind === 'string' && value.kind.length > 0 ? value.kind : 'custom',
|
|
111
|
+
prompt: promptFrom(value),
|
|
112
|
+
requester: isRecord(value.requester) ? value.requester as InteractionRequester : undefined,
|
|
113
|
+
} as SubagentInteractionRequest;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const handle = value.handle;
|
|
117
|
+
if (typeof handle === 'string') {
|
|
118
|
+
const resolved = resolveInteractionRequest(handle);
|
|
119
|
+
if (resolved) return resolved;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const child of Object.values(value)) {
|
|
123
|
+
const found = findInteractionRequest(child, seen);
|
|
124
|
+
if (found) return found;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function interactionRequestFromCandidate(value: unknown): SubagentInteractionRequest | undefined {
|
|
131
|
+
return findInteractionRequest(value, new Set());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function publishInteractionRequest(payload: SubagentInteractionRequest): PublishedInteractionRequest {
|
|
135
|
+
const published: PublishedInteractionRequest = {
|
|
136
|
+
handle: `interaction_${randomUUID().replace(/-/g, '')}`,
|
|
137
|
+
payload,
|
|
138
|
+
createdAt: new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
requestRegistry().set(published.handle, published);
|
|
141
|
+
return published;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resolveInteractionRequest(handle: string): SubagentInteractionRequest | undefined {
|
|
145
|
+
return requestRegistry().get(handle)?.payload;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function consumeInteractionRequest(handle: string): SubagentInteractionRequest | undefined {
|
|
149
|
+
const registry = requestRegistry();
|
|
150
|
+
const published = registry.get(handle);
|
|
151
|
+
if (!published) return undefined;
|
|
152
|
+
registry.delete(handle);
|
|
153
|
+
return published.payload;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function consumeLatestInteractionRequest(options: { maxAgeMs?: number; origin?: string } = {}): SubagentInteractionRequest | undefined {
|
|
157
|
+
const maxAgeMs = options.maxAgeMs ?? 30_000;
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const candidates = [...requestRegistry().entries()]
|
|
160
|
+
.map(([handle, published]) => ({ handle, published, timestamp: Date.parse(published.createdAt) }))
|
|
161
|
+
.filter(({ published, timestamp }) => Number.isFinite(timestamp)
|
|
162
|
+
&& now - timestamp <= maxAgeMs
|
|
163
|
+
&& (!options.origin || published.payload.origin === options.origin))
|
|
164
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
165
|
+
const latest = candidates[0];
|
|
166
|
+
if (!latest) return undefined;
|
|
167
|
+
requestRegistry().delete(latest.handle);
|
|
168
|
+
return latest.published.payload;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function publishInteractionResponse(response: SubagentInteractionResponse): SubagentInteractionResponse {
|
|
172
|
+
const normalized: SubagentInteractionResponse = {
|
|
173
|
+
responder: 'parent',
|
|
174
|
+
answeredAt: new Date().toISOString(),
|
|
175
|
+
...response,
|
|
176
|
+
type: 'interaction_response',
|
|
177
|
+
};
|
|
178
|
+
responseRegistry().set(normalized.requestId, normalized);
|
|
179
|
+
return normalized;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function resolveInteractionResponse(requestId: string): SubagentInteractionResponse | undefined {
|
|
183
|
+
return responseRegistry().get(requestId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function consumeInteractionResponse(requestId: string): SubagentInteractionResponse | undefined {
|
|
187
|
+
const registry = responseRegistry();
|
|
188
|
+
const response = registry.get(requestId);
|
|
189
|
+
if (!response) return undefined;
|
|
190
|
+
registry.delete(requestId);
|
|
191
|
+
return response;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function sanitizeInteractionTransportText(text: string): string {
|
|
195
|
+
if (!text) return text;
|
|
196
|
+
return text.replace(/interaction_required:[^\r\n]*/g, '[interaction request hidden]');
|
|
197
|
+
}
|