@parlor/agent-host 0.3.0 → 0.3.2
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/dist/agent-options.d.ts +5 -0
- package/dist/agent-options.d.ts.map +1 -1
- package/dist/agent-options.js +32 -0
- package/dist/agent-options.js.map +1 -1
- package/dist/agent-skills.d.ts +15 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/agent-skills.js +92 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/agent-survey.d.ts +15 -0
- package/dist/agent-survey.d.ts.map +1 -0
- package/dist/agent-survey.js +51 -0
- package/dist/agent-survey.js.map +1 -0
- package/dist/agents.d.ts +15 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +33 -9
- package/dist/agents.js.map +1 -1
- package/dist/codex-app-server.d.ts +7 -0
- package/dist/codex-app-server.d.ts.map +1 -0
- package/dist/codex-app-server.js +46 -0
- package/dist/codex-app-server.js.map +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +192 -26
- package/dist/daemon.js.map +1 -1
- package/dist/learned.d.ts +2 -0
- package/dist/learned.d.ts.map +1 -1
- package/dist/learned.js +1 -0
- package/dist/learned.js.map +1 -1
- package/dist/link.d.ts +5 -1
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +25 -1
- package/dist/link.js.map +1 -1
- package/dist/mcp-proxy.d.ts.map +1 -1
- package/dist/mcp-proxy.js +109 -17
- package/dist/mcp-proxy.js.map +1 -1
- package/dist/shell-path.d.ts +2 -0
- package/dist/shell-path.d.ts.map +1 -1
- package/dist/shell-path.js +14 -3
- package/dist/shell-path.js.map +1 -1
- package/dist/spec-kit.d.ts +4 -0
- package/dist/spec-kit.d.ts.map +1 -1
- package/dist/spec-kit.js +13 -1
- package/dist/spec-kit.js.map +1 -1
- package/package.json +9 -9
package/dist/daemon.js
CHANGED
|
@@ -3,8 +3,10 @@ import { createServer } from "node:net";
|
|
|
3
3
|
import { basename, delimiter, dirname, join } from "node:path";
|
|
4
4
|
import { HOST_PROTOCOL, } from "@parlor/chat";
|
|
5
5
|
import { ActionError, asId, DEV_USER_ID } from "@parlor/core";
|
|
6
|
+
import { removeAgentSkills, writeAgentSkills } from "./agent-skills.js";
|
|
7
|
+
import { readUsage, surveyOptions } from "./agent-survey.js";
|
|
6
8
|
import { agentNames, registerAgentTools, unregisterAgentTools, } from "./agent-tools.js";
|
|
7
|
-
import {
|
|
9
|
+
import { findAgents, loadProfiles } from "./agents.js";
|
|
8
10
|
import { connectControl } from "./control.js";
|
|
9
11
|
import { requestDevHostToken } from "./dev-host.js";
|
|
10
12
|
import { devHostKeys } from "./grants.js";
|
|
@@ -21,7 +23,7 @@ import { compareVersions, RELEASE_KEY, signedRelease } from "./release.js";
|
|
|
21
23
|
import { revokeGrant } from "./revoke.js";
|
|
22
24
|
import { selfEntry } from "./self.js";
|
|
23
25
|
import { installLoginItem, installRelease } from "./service.js";
|
|
24
|
-
import { agentEnvironment } from "./shell-path.js";
|
|
26
|
+
import { agentEnvironment, rereadAgentPath } from "./shell-path.js";
|
|
25
27
|
import { runTurn } from "./turn.js";
|
|
26
28
|
const CONNECT_TIMEOUT_MS = 15_000;
|
|
27
29
|
/** An agent token is asked for again this long before it runs out. */
|
|
@@ -63,12 +65,16 @@ export class HostAlreadyRunning extends Error {
|
|
|
63
65
|
super("Parlor Host is already running on this computer.");
|
|
64
66
|
}
|
|
65
67
|
}
|
|
68
|
+
/** Codex's own CLI throttles its rate-limit reports on the same interval. */
|
|
69
|
+
const USAGE_READ_FLOOR_MS = 30_000;
|
|
66
70
|
export function createHostDaemon(deps) {
|
|
67
71
|
const { paths } = deps;
|
|
68
72
|
const links = new Map();
|
|
69
73
|
const devTokens = new Map();
|
|
70
74
|
const name = computerName();
|
|
71
75
|
const platform = platformName();
|
|
76
|
+
/** As `agents.json` lists them; `profiles` are these with each `binary` set to the CLI found. */
|
|
77
|
+
let listedProfiles = [];
|
|
72
78
|
let profiles = [];
|
|
73
79
|
let agents = [];
|
|
74
80
|
const learned = learnedStore(paths.learned);
|
|
@@ -89,14 +95,75 @@ export function createHostDaemon(deps) {
|
|
|
89
95
|
return undefined;
|
|
90
96
|
}
|
|
91
97
|
};
|
|
98
|
+
/** What an agent said about itself, told to every linked app when it changes what they show. */
|
|
99
|
+
const learn = (agentId, news) => {
|
|
100
|
+
void learned
|
|
101
|
+
.learn(agentId, news)
|
|
102
|
+
.then((changed) => {
|
|
103
|
+
if (!changed)
|
|
104
|
+
return;
|
|
105
|
+
for (const linked of links.values())
|
|
106
|
+
void report(linked).catch(() => undefined);
|
|
107
|
+
})
|
|
108
|
+
.catch(() => undefined);
|
|
109
|
+
};
|
|
110
|
+
/** Ends the reads below on stop or handover, so nothing outlives the daemon. */
|
|
111
|
+
const stopping = new AbortController();
|
|
112
|
+
const usageReadAt = new Map();
|
|
113
|
+
/** The allowance from the agent's own CLI where ACP carries none, at most every 30s per agent. */
|
|
114
|
+
const refreshUsage = async (agentId) => {
|
|
115
|
+
const profile = profiles.find((candidate) => candidate.id === agentId);
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
if (!profile || stopping.signal.aborted)
|
|
118
|
+
return;
|
|
119
|
+
if (now - (usageReadAt.get(agentId) ?? 0) < USAGE_READ_FLOOR_MS)
|
|
120
|
+
return;
|
|
121
|
+
usageReadAt.set(agentId, now);
|
|
122
|
+
const usage = await readUsage(profile, await agentEnvironment(), now).catch(() => undefined);
|
|
123
|
+
if (usage && !stopping.signal.aborted)
|
|
124
|
+
learn(agentId, { usage });
|
|
125
|
+
};
|
|
126
|
+
let surveyed = false;
|
|
127
|
+
/**
|
|
128
|
+
* Options for every agent that has not run a turn yet, so the sidebar can offer a model first.
|
|
129
|
+
* Once per run, when an app first links: a computer no app uses never starts its agents.
|
|
130
|
+
*/
|
|
131
|
+
const surveyAgents = async () => {
|
|
132
|
+
if (surveyed)
|
|
133
|
+
return;
|
|
134
|
+
surveyed = true;
|
|
135
|
+
const env = await agentEnvironment();
|
|
136
|
+
for (const agent of agents) {
|
|
137
|
+
const profile = profiles.find((candidate) => candidate.id === agent.id);
|
|
138
|
+
if (!profile || stopping.signal.aborted)
|
|
139
|
+
continue;
|
|
140
|
+
void refreshUsage(agent.id);
|
|
141
|
+
if (learned.knows(agent.id))
|
|
142
|
+
continue;
|
|
143
|
+
const options = await surveyOptions(profile, deps.launcher, env, stopping.signal);
|
|
144
|
+
if (stopping.signal.aborted)
|
|
145
|
+
return;
|
|
146
|
+
logEvent("agent_surveyed", { agentId: agent.id, options: options?.length ?? 0 });
|
|
147
|
+
if (options && options.length > 0)
|
|
148
|
+
learn(agent.id, { options });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
92
151
|
const running = new Set();
|
|
93
152
|
let handedOver = false;
|
|
94
153
|
let finishing = false;
|
|
95
154
|
let finishHandover = () => undefined;
|
|
96
155
|
const drained = new Promise((resolve) => (finishHandover = resolve));
|
|
97
|
-
|
|
156
|
+
/** A turn runs once here, even when a pushed turn its app took back is claimed again. */
|
|
157
|
+
const runningTurns = new Set();
|
|
158
|
+
const run = (state, claimed, how = "claimed") => {
|
|
98
159
|
const { link } = state;
|
|
99
|
-
|
|
160
|
+
if (runningTurns.has(claimed.turnId))
|
|
161
|
+
return;
|
|
162
|
+
runningTurns.add(claimed.turnId);
|
|
163
|
+
logEvent(how === "pushed" ? "turn_pushed" : "turn_claimed", {
|
|
164
|
+
appOrigin: link.grant.appOrigin,
|
|
165
|
+
turnId: claimed.turnId,
|
|
166
|
+
});
|
|
100
167
|
state.turns += 1;
|
|
101
168
|
const turn = (link.grant.dev ? Promise.resolve(DEV_POLICY) : loadPolicy(paths.policy, deps.policy))
|
|
102
169
|
.then((policy) => runTurn(claimed, {
|
|
@@ -119,17 +186,7 @@ export function createHostDaemon(deps) {
|
|
|
119
186
|
policy,
|
|
120
187
|
prompter: deps.prompter,
|
|
121
188
|
now: Date.now,
|
|
122
|
-
learned:
|
|
123
|
-
void learned
|
|
124
|
-
.learn(agentId, news)
|
|
125
|
-
.then((changed) => {
|
|
126
|
-
if (!changed)
|
|
127
|
-
return;
|
|
128
|
-
for (const linked of links.values())
|
|
129
|
-
void report(linked).catch(() => undefined);
|
|
130
|
-
})
|
|
131
|
-
.catch(() => undefined);
|
|
132
|
-
},
|
|
189
|
+
learned: learn,
|
|
133
190
|
pairing: {
|
|
134
191
|
secret: async () => (await deps.grants.get(link.grant.appOrigin))?.pairingSecret,
|
|
135
192
|
forget: async () => {
|
|
@@ -143,6 +200,8 @@ export function createHostDaemon(deps) {
|
|
|
143
200
|
}))
|
|
144
201
|
.then((outcome) => logEvent("turn_finished", { turnId: claimed.turnId, outcome }), (error) => logEvent("turn_finished", { turnId: claimed.turnId, error: String(error) }))
|
|
145
202
|
.finally(() => {
|
|
203
|
+
runningTurns.delete(claimed.turnId);
|
|
204
|
+
void refreshUsage(claimed.agent.agentId);
|
|
146
205
|
state.turns -= 1;
|
|
147
206
|
running.delete(turn);
|
|
148
207
|
settleHandover();
|
|
@@ -190,21 +249,112 @@ export function createHostDaemon(deps) {
|
|
|
190
249
|
agents: agentNames(profiles, { name: tools.name, agents: tools.agents }),
|
|
191
250
|
};
|
|
192
251
|
};
|
|
252
|
+
/** The skill that sends the agents to the app's tools when the user names it, kept current. */
|
|
253
|
+
const writeSkills = async (grant) => {
|
|
254
|
+
const tools = grant.agentTools;
|
|
255
|
+
if (grant.dev || !tools?.on || !tools.name || !tools.agents?.length)
|
|
256
|
+
return;
|
|
257
|
+
await writeAgentSkills(tools.agents, {
|
|
258
|
+
name: tools.name,
|
|
259
|
+
appName: grant.appName,
|
|
260
|
+
appOrigin: grant.appOrigin,
|
|
261
|
+
account: grant.account.email ?? grant.account.name,
|
|
262
|
+
}, await agentEnvironment());
|
|
263
|
+
};
|
|
264
|
+
let toolsWork = Promise.resolve();
|
|
265
|
+
/** Registering and removing the app with the agents runs one at a time, on the grant as saved by then. */
|
|
266
|
+
const serially = (work) => {
|
|
267
|
+
const next = toolsWork.then(work, work);
|
|
268
|
+
toolsWork = next.catch(() => undefined);
|
|
269
|
+
return next;
|
|
270
|
+
};
|
|
193
271
|
/** Registers the app with the agents here once the user chose it and it is not yet. */
|
|
194
|
-
const ensureAgentTools =
|
|
195
|
-
|
|
272
|
+
const ensureAgentTools = (grant) => serially(() => ensureAgentToolsNow(grant));
|
|
273
|
+
const ensureAgentToolsNow = async (grant) => {
|
|
274
|
+
if (!grant.agentTools?.on || grant.dev)
|
|
275
|
+
return grant;
|
|
276
|
+
if (grant.agentTools.agents) {
|
|
277
|
+
await writeSkills(grant);
|
|
196
278
|
return grant;
|
|
279
|
+
}
|
|
197
280
|
const registered = await registerAgentTools(toolsDeps(), grant);
|
|
198
281
|
const next = { ...grant, agentTools: { on: true, ...registered } };
|
|
199
282
|
await deps.grants.save(next);
|
|
283
|
+
await writeSkills(next);
|
|
200
284
|
return next;
|
|
201
285
|
};
|
|
202
|
-
|
|
286
|
+
/** Hands the app to agents installed since it was registered with the ones here then. */
|
|
287
|
+
const handToNewAgents = (origin) => serially(async () => {
|
|
288
|
+
const grant = await deps.grants.get(origin);
|
|
289
|
+
if (grant)
|
|
290
|
+
await handToNewAgentsNow(grant);
|
|
291
|
+
});
|
|
292
|
+
const handToNewAgentsNow = async (grant) => {
|
|
293
|
+
const tools = grant.agentTools;
|
|
294
|
+
if (grant.declined || grant.dev || !tools?.on || !tools.agents)
|
|
295
|
+
return;
|
|
296
|
+
const have = new Set(tools.agents);
|
|
297
|
+
const fresh = toolsDeps().profiles.filter((profile) => !have.has(profile.id));
|
|
298
|
+
if (fresh.length === 0) {
|
|
299
|
+
await writeSkills(grant);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const registered = await registerAgentTools({ ...toolsDeps(), profiles: fresh }, grant);
|
|
303
|
+
if (registered.agents.length === 0) {
|
|
304
|
+
await writeSkills(grant);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const next = {
|
|
308
|
+
...grant,
|
|
309
|
+
agentTools: {
|
|
310
|
+
on: true,
|
|
311
|
+
name: registered.name,
|
|
312
|
+
agents: [...tools.agents, ...registered.agents],
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
await deps.grants.save(next);
|
|
316
|
+
latest.set(grant.appOrigin, next);
|
|
317
|
+
await writeSkills(next);
|
|
318
|
+
};
|
|
319
|
+
let checking;
|
|
320
|
+
let checkAgain = false;
|
|
321
|
+
/**
|
|
322
|
+
* Looks for the agents again because a tab asked: one installed since this computer
|
|
323
|
+
* started shows in every app's sidebar, and gets the apps the user let its agents use.
|
|
324
|
+
*/
|
|
325
|
+
const checkAgents = () => {
|
|
326
|
+
if (checking) {
|
|
327
|
+
checkAgain = true;
|
|
328
|
+
return checking;
|
|
329
|
+
}
|
|
330
|
+
checking = (async () => {
|
|
331
|
+
do {
|
|
332
|
+
checkAgain = false;
|
|
333
|
+
await rereadAgentPath();
|
|
334
|
+
const found = await findAgents(listedProfiles);
|
|
335
|
+
profiles = found.profiles;
|
|
336
|
+
agents = [...found.agents];
|
|
337
|
+
for (const grant of await deps.grants.list()) {
|
|
338
|
+
await handToNewAgents(grant.appOrigin).catch((error) => logEvent("agent_tools_failed", {
|
|
339
|
+
appOrigin: grant.appOrigin,
|
|
340
|
+
error: String(error),
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
await Promise.all([...links.values()].map((state) => report(state, true).catch(() => undefined)));
|
|
344
|
+
logEvent("agents_checked", { agents: agents.map((agent) => agent.id) });
|
|
345
|
+
} while (checkAgain);
|
|
346
|
+
})().finally(() => {
|
|
347
|
+
checking = undefined;
|
|
348
|
+
});
|
|
349
|
+
return checking;
|
|
350
|
+
};
|
|
351
|
+
const removeAgentTools = (grant) => serially(async () => {
|
|
203
352
|
const tools = grant?.agentTools;
|
|
204
353
|
if (!tools?.name || !tools.agents?.length)
|
|
205
354
|
return;
|
|
206
355
|
await unregisterAgentTools(toolsDeps(), { name: tools.name, agents: tools.agents });
|
|
207
|
-
|
|
356
|
+
await removeAgentSkills(tools.agents, tools.name, await agentEnvironment());
|
|
357
|
+
});
|
|
208
358
|
const agentTokens = new Map();
|
|
209
359
|
/** The grant as last saved, which a link opened before a change still holds the old copy of. */
|
|
210
360
|
const latest = new Map();
|
|
@@ -256,6 +406,7 @@ export function createHostDaemon(deps) {
|
|
|
256
406
|
await deps.pool.forget(grant);
|
|
257
407
|
};
|
|
258
408
|
const open = (grant, keys) => {
|
|
409
|
+
void surveyAgents().catch(() => undefined);
|
|
259
410
|
const origin = grant.appOrigin;
|
|
260
411
|
latest.set(origin, grant);
|
|
261
412
|
let state;
|
|
@@ -266,6 +417,14 @@ export function createHostDaemon(deps) {
|
|
|
266
417
|
platform,
|
|
267
418
|
fetch: (input, init) => fetch(input, init),
|
|
268
419
|
onWake: () => wake(state),
|
|
420
|
+
onAgentsCheck: () => void checkAgents(),
|
|
421
|
+
onPushed: (_link, turn) => {
|
|
422
|
+
// Tells the app this computer has the turn, while it already starts on it.
|
|
423
|
+
void link.api.calls
|
|
424
|
+
.call("hostAppend", { turnId: turn.turnId, items: [] })
|
|
425
|
+
.catch(() => undefined);
|
|
426
|
+
run(state, turn, "pushed");
|
|
427
|
+
},
|
|
269
428
|
busy: () => state.turns > 0,
|
|
270
429
|
onRevoked: () => {
|
|
271
430
|
if (links.get(origin) !== state)
|
|
@@ -320,11 +479,13 @@ export function createHostDaemon(deps) {
|
|
|
320
479
|
? { ...agent, outsideName: tools.name }
|
|
321
480
|
: agent);
|
|
322
481
|
};
|
|
323
|
-
|
|
482
|
+
/** `agentsChecked` when the agents were just looked for, which a tab waiting on a check reads. */
|
|
483
|
+
const report = async (state, agentsChecked = false) => {
|
|
324
484
|
const reported = state.link.api.calls.call("hostReport", {
|
|
325
485
|
online: true,
|
|
326
486
|
agents: describeFor(state.link.grant.appOrigin),
|
|
327
487
|
...(await pinnedDevices(state)),
|
|
488
|
+
...(agentsChecked ? { agentsChecked: true } : {}),
|
|
328
489
|
});
|
|
329
490
|
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("The app did not answer in time.")), CONNECT_TIMEOUT_MS));
|
|
330
491
|
try {
|
|
@@ -352,7 +513,7 @@ export function createHostDaemon(deps) {
|
|
|
352
513
|
const attach = async (origin, hostToken) => {
|
|
353
514
|
await closeLink(origin, "replaced");
|
|
354
515
|
devTokens.set(origin, hostToken);
|
|
355
|
-
await report(open(await devGrant(origin), devHostKeys(hostToken)));
|
|
516
|
+
await report(open(await devGrant(origin), devHostKeys(hostToken)), true);
|
|
356
517
|
};
|
|
357
518
|
const reattach = async (grant) => {
|
|
358
519
|
await closeLink(grant.appOrigin, "dev_server_restarted");
|
|
@@ -407,8 +568,10 @@ export function createHostDaemon(deps) {
|
|
|
407
568
|
openHostLog(join(paths.root, "logs", "daemon.log"));
|
|
408
569
|
await putHostCommandFirstOnPath(paths.root);
|
|
409
570
|
await deps.proxy.listen(paths.proxy);
|
|
410
|
-
|
|
411
|
-
|
|
571
|
+
listedProfiles = await loadProfiles(paths.agents);
|
|
572
|
+
const found = await findAgents(listedProfiles);
|
|
573
|
+
profiles = found.profiles;
|
|
574
|
+
agents = [...found.agents];
|
|
412
575
|
await learned.load();
|
|
413
576
|
finishStartup();
|
|
414
577
|
}
|
|
@@ -423,7 +586,8 @@ export function createHostDaemon(deps) {
|
|
|
423
586
|
for (const grant of await deps.grants.list()) {
|
|
424
587
|
if (grant.declined)
|
|
425
588
|
continue;
|
|
426
|
-
void
|
|
589
|
+
void handToNewAgents(grant.appOrigin).catch(() => undefined);
|
|
590
|
+
void report(open(grant, deps.keys), true).catch((error) => logEvent("link_failed", { appOrigin: grant.appOrigin, error: String(error) }));
|
|
427
591
|
}
|
|
428
592
|
},
|
|
429
593
|
async connect(appOrigin) {
|
|
@@ -435,14 +599,14 @@ export function createHostDaemon(deps) {
|
|
|
435
599
|
const current = links.get(appOrigin);
|
|
436
600
|
if (current?.link.grant.grantId === grant.grantId) {
|
|
437
601
|
latest.set(appOrigin, grant);
|
|
438
|
-
await report(current);
|
|
602
|
+
await report(current, true);
|
|
439
603
|
return toolsOf(grant);
|
|
440
604
|
}
|
|
441
605
|
await closeLink(appOrigin, "replaced");
|
|
442
606
|
if (current)
|
|
443
607
|
await deps.pool.forget(current.link.grant);
|
|
444
608
|
latest.set(appOrigin, grant);
|
|
445
|
-
await report(open(grant, deps.keys));
|
|
609
|
+
await report(open(grant, deps.keys), true);
|
|
446
610
|
return toolsOf(grant);
|
|
447
611
|
},
|
|
448
612
|
async agentTools(appOrigin, on) {
|
|
@@ -485,6 +649,7 @@ export function createHostDaemon(deps) {
|
|
|
485
649
|
if (handedOver)
|
|
486
650
|
return;
|
|
487
651
|
handedOver = true;
|
|
652
|
+
stopping.abort();
|
|
488
653
|
for (const state of links.values())
|
|
489
654
|
state.link.drain();
|
|
490
655
|
await Promise.all([...links.values()].map((state) => state.claiming));
|
|
@@ -495,6 +660,7 @@ export function createHostDaemon(deps) {
|
|
|
495
660
|
logEvent("handover_started", { version: deps.release.version });
|
|
496
661
|
},
|
|
497
662
|
async stop() {
|
|
663
|
+
stopping.abort();
|
|
498
664
|
if (!handedOver) {
|
|
499
665
|
await Promise.all([...links.values()].map((state) => Promise.race([
|
|
500
666
|
state.link.api.calls.call("hostReport", {
|