@adecore/agents 0.39.0 → 0.40.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/dist/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.js +0 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.js +6 -8
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +208 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +401 -10
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
package/src/skills.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { open, readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join, parse } from 'node:path';
|
|
5
5
|
import type { AgentKind, ChatSkill, ChatSkillSource } from '@adecore/agent-contracts';
|
|
6
|
-
import {
|
|
6
|
+
import { wait } from './async.ts';
|
|
7
|
+
import { hasErrorCode, isNotFound } from './fs.ts';
|
|
7
8
|
|
|
8
9
|
export interface SkillRoot {
|
|
9
10
|
dir: string;
|
|
@@ -62,7 +63,6 @@ const HEAD_BYTES = 64 * 1024;
|
|
|
62
63
|
const MAX_DESCRIPTION = 1024;
|
|
63
64
|
|
|
64
65
|
export interface SkillRootOptions {
|
|
65
|
-
// The person's home directory; a test points it at a temporary tree.
|
|
66
66
|
home?: string;
|
|
67
67
|
// Where Claude Code keeps its own configuration, when it is not `<home>/.claude`.
|
|
68
68
|
claudeConfigDir?: string;
|
|
@@ -72,6 +72,7 @@ const NAME = /^[A-Za-z][A-Za-z0-9_:-]*$/;
|
|
|
72
72
|
|
|
73
73
|
function unquote(value: string): string {
|
|
74
74
|
const quoted = /^(['"])([\s\S]*)\1$/.exec(value);
|
|
75
|
+
|
|
75
76
|
return quoted ? quoted[2]! : value;
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -81,28 +82,38 @@ function unquote(value: string): string {
|
|
|
81
82
|
*/
|
|
82
83
|
export function parseFrontmatter(text: string): Record<string, string> {
|
|
83
84
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
85
|
+
|
|
84
86
|
if (!match) {
|
|
85
87
|
return {};
|
|
86
88
|
}
|
|
89
|
+
|
|
87
90
|
const fields: Record<string, string> = {};
|
|
88
91
|
const lines = match[1]!.split(/\r?\n/);
|
|
92
|
+
|
|
89
93
|
for (let i = 0; i < lines.length; i++) {
|
|
90
94
|
const pair = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i]!);
|
|
95
|
+
|
|
91
96
|
if (!pair) {
|
|
92
97
|
continue;
|
|
93
98
|
}
|
|
99
|
+
|
|
94
100
|
let value = pair[2]!.trim();
|
|
101
|
+
|
|
95
102
|
if (value === '' || value === '>' || value === '>-' || value === '|' || value === '|-') {
|
|
96
103
|
// A block scalar puts the value on the indented lines under it; `|` keeps the newlines.
|
|
97
104
|
const keepNewlines = value.startsWith('|');
|
|
98
105
|
const parts: string[] = [];
|
|
106
|
+
|
|
99
107
|
while (i + 1 < lines.length && /^[ \t]+\S/.test(lines[i + 1]!)) {
|
|
100
108
|
parts.push(lines[++i]!.trim());
|
|
101
109
|
}
|
|
110
|
+
|
|
102
111
|
value = parts.join(keepNewlines ? '\n' : ' ');
|
|
103
112
|
}
|
|
113
|
+
|
|
104
114
|
fields[pair[1]!] = unquote(value);
|
|
105
115
|
}
|
|
116
|
+
|
|
106
117
|
return fields;
|
|
107
118
|
}
|
|
108
119
|
|
|
@@ -113,58 +124,76 @@ export function parseFrontmatter(text: string): Record<string, string> {
|
|
|
113
124
|
function projectDirs(cwd: string, home: string): string[] {
|
|
114
125
|
const dirs: string[] = [];
|
|
115
126
|
let current = cwd;
|
|
127
|
+
|
|
116
128
|
while (current !== '' && current !== parse(current).root && current !== home && current !== dirname(home) && dirs.length < 8) {
|
|
117
129
|
dirs.push(current);
|
|
130
|
+
|
|
118
131
|
// Anything above a repository root belongs to another project, so the walk ends there.
|
|
119
132
|
if (existsSync(join(current, '.git'))) {
|
|
120
133
|
break;
|
|
121
134
|
}
|
|
135
|
+
|
|
122
136
|
const parent = dirname(current);
|
|
137
|
+
|
|
123
138
|
if (parent === current) {
|
|
124
139
|
break;
|
|
125
140
|
}
|
|
141
|
+
|
|
126
142
|
current = parent;
|
|
127
143
|
}
|
|
144
|
+
|
|
128
145
|
return dirs;
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
/* The plugin folders Claude Code installed, each named the way its skills are addressed. */
|
|
132
149
|
export async function pluginSkillRoots(configDir: string): Promise<SkillRoot[]> {
|
|
133
150
|
let raw: string;
|
|
151
|
+
|
|
134
152
|
try {
|
|
135
153
|
raw = await readFile(join(configDir, 'plugins', 'installed_plugins.json'), 'utf8');
|
|
136
154
|
} catch (e) {
|
|
137
155
|
if (isNotFound(e)) {
|
|
138
156
|
return [];
|
|
139
157
|
}
|
|
158
|
+
|
|
140
159
|
throw e;
|
|
141
160
|
}
|
|
161
|
+
|
|
142
162
|
let parsed: unknown;
|
|
163
|
+
|
|
143
164
|
try {
|
|
144
165
|
parsed = JSON.parse(raw);
|
|
145
166
|
} catch {
|
|
146
167
|
return [];
|
|
147
168
|
}
|
|
169
|
+
|
|
148
170
|
const plugins = typeof parsed === 'object' && parsed !== null ? (parsed as { plugins?: unknown }).plugins : undefined;
|
|
171
|
+
|
|
149
172
|
if (typeof plugins !== 'object' || plugins === null) {
|
|
150
173
|
return [];
|
|
151
174
|
}
|
|
175
|
+
|
|
152
176
|
const roots: SkillRoot[] = [];
|
|
177
|
+
|
|
153
178
|
for (const [key, installs] of Object.entries(plugins as Record<string, unknown>)) {
|
|
154
179
|
const name = key.split('@')[0] ?? key;
|
|
180
|
+
|
|
155
181
|
for (const install of Array.isArray(installs) ? installs : []) {
|
|
156
182
|
const path = typeof install === 'object' && install !== null ? (install as { installPath?: unknown }).installPath : undefined;
|
|
183
|
+
|
|
157
184
|
if (typeof path === 'string' && path !== '') {
|
|
158
185
|
roots.push({ dir: join(path, 'skills'), source: 'plugin', prefix: name });
|
|
159
186
|
}
|
|
160
187
|
}
|
|
161
188
|
}
|
|
189
|
+
|
|
162
190
|
return roots;
|
|
163
191
|
}
|
|
164
192
|
|
|
165
193
|
/* Where a provider's CLI looks for skills, in the order a name clash is decided. */
|
|
166
194
|
export async function skillRootsFor(kind: AgentKind, cwd: string, options: SkillRootOptions = {}): Promise<SkillRoot[]> {
|
|
167
195
|
const home = options.home ?? homedir();
|
|
196
|
+
|
|
168
197
|
if (kind === 'codex') {
|
|
169
198
|
return [
|
|
170
199
|
{ dir: join(home, '.agents', 'skills'), source: 'user', depth: CODEX_SKILL_DEPTH },
|
|
@@ -172,7 +201,9 @@ export async function skillRootsFor(kind: AgentKind, cwd: string, options: Skill
|
|
|
172
201
|
...projectDirs(cwd, home).map((dir): SkillRoot => ({ dir: join(dir, '.agents', 'skills'), source: 'project', depth: CODEX_SKILL_DEPTH }))
|
|
173
202
|
];
|
|
174
203
|
}
|
|
204
|
+
|
|
175
205
|
const configDir = options.claudeConfigDir ?? join(home, '.claude');
|
|
206
|
+
|
|
176
207
|
return [
|
|
177
208
|
{ dir: join(configDir, 'skills'), source: 'user' },
|
|
178
209
|
...projectDirs(cwd, home).map((dir): SkillRoot => ({ dir: join(dir, '.claude', 'skills'), source: 'project' })),
|
|
@@ -182,15 +213,11 @@ export async function skillRootsFor(kind: AgentKind, cwd: string, options: Skill
|
|
|
182
213
|
|
|
183
214
|
/* Too many files open, in this process or on the system: it says nothing about the path that was asked for. */
|
|
184
215
|
function isBusy(e: unknown): boolean {
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function sleep(ms: number): Promise<void> {
|
|
189
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
216
|
+
return hasErrorCode(e, 'EMFILE', 'ENFILE');
|
|
190
217
|
}
|
|
191
218
|
|
|
192
219
|
/* Runs `operation` again a few times while the process is short of file handles, and throws if it still is. */
|
|
193
|
-
export async function whileBusy<T>(operation: () => Promise<T>,
|
|
220
|
+
export async function whileBusy<T>(operation: () => Promise<T>, sleep: (ms: number) => Promise<void> = wait): Promise<T> {
|
|
194
221
|
for (let attempt = 1; ; attempt++) {
|
|
195
222
|
try {
|
|
196
223
|
return await operation();
|
|
@@ -198,7 +225,8 @@ export async function whileBusy<T>(operation: () => Promise<T>, wait: (ms: numbe
|
|
|
198
225
|
if (!isBusy(e) || attempt > BUSY_RETRIES) {
|
|
199
226
|
throw e;
|
|
200
227
|
}
|
|
201
|
-
|
|
228
|
+
|
|
229
|
+
await sleep(BUSY_RETRY_MS * attempt);
|
|
202
230
|
}
|
|
203
231
|
}
|
|
204
232
|
}
|
|
@@ -207,33 +235,41 @@ export async function whileBusy<T>(operation: () => Promise<T>, wait: (ms: numbe
|
|
|
207
235
|
export async function mapLimited<T, R>(items: readonly T[], limit: number, work: (item: T) => Promise<R>): Promise<R[]> {
|
|
208
236
|
const results = new Array<R>(items.length);
|
|
209
237
|
let next = 0;
|
|
238
|
+
|
|
210
239
|
const worker = async (): Promise<void> => {
|
|
211
240
|
while (next < items.length) {
|
|
212
241
|
const index = next++;
|
|
213
242
|
results[index] = await work(items[index]!);
|
|
214
243
|
}
|
|
215
244
|
};
|
|
245
|
+
|
|
216
246
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
247
|
+
|
|
217
248
|
return results;
|
|
218
249
|
}
|
|
219
250
|
|
|
220
251
|
/* The top of a folder's SKILL.md, or null when it has none. A process short of file handles throws instead of finding none. */
|
|
221
252
|
async function skillHead(folder: string): Promise<string | null> {
|
|
222
253
|
let handle;
|
|
254
|
+
|
|
223
255
|
try {
|
|
224
256
|
handle = await whileBusy(() => open(join(folder, 'SKILL.md'), 'r'));
|
|
225
257
|
} catch (e) {
|
|
226
258
|
if (isBusy(e)) {
|
|
227
259
|
throw e;
|
|
228
260
|
}
|
|
261
|
+
|
|
229
262
|
return null;
|
|
230
263
|
}
|
|
264
|
+
|
|
231
265
|
try {
|
|
232
266
|
if (!(await handle.stat()).isFile()) {
|
|
233
267
|
return null;
|
|
234
268
|
}
|
|
269
|
+
|
|
235
270
|
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
236
271
|
const { bytesRead } = await handle.read(buffer, 0, HEAD_BYTES, 0);
|
|
272
|
+
|
|
237
273
|
return buffer.subarray(0, bytesRead).toString('utf8');
|
|
238
274
|
} catch {
|
|
239
275
|
return null;
|
|
@@ -246,9 +282,11 @@ function skillOf(root: SkillRoot, folder: string, text: string): ChatSkill | nul
|
|
|
246
282
|
const fields = parseFrontmatter(text);
|
|
247
283
|
const own = fields.name && NAME.test(fields.name) ? fields.name : folder;
|
|
248
284
|
const name = root.prefix ? `${root.prefix}:${own}` : own;
|
|
285
|
+
|
|
249
286
|
if (!NAME.test(name)) {
|
|
250
287
|
return null;
|
|
251
288
|
}
|
|
289
|
+
|
|
252
290
|
return { name, description: (fields.description ?? '').slice(0, MAX_DESCRIPTION), source: root.source };
|
|
253
291
|
}
|
|
254
292
|
|
|
@@ -256,11 +294,13 @@ function skillOf(root: SkillRoot, folder: string, text: string): ChatSkill | nul
|
|
|
256
294
|
async function realFolder(path: string): Promise<string | null> {
|
|
257
295
|
try {
|
|
258
296
|
const real = await whileBusy(() => realpath(path));
|
|
297
|
+
|
|
259
298
|
return (await whileBusy(() => stat(real))).isDirectory() ? real : null;
|
|
260
299
|
} catch (e) {
|
|
261
300
|
if (isBusy(e)) {
|
|
262
301
|
throw e;
|
|
263
302
|
}
|
|
303
|
+
|
|
264
304
|
return null;
|
|
265
305
|
}
|
|
266
306
|
}
|
|
@@ -272,52 +312,68 @@ async function realFolder(path: string): Promise<string | null> {
|
|
|
272
312
|
*/
|
|
273
313
|
async function scanRoot(root: SkillRoot, limits: SkillScanLimits): Promise<ChatSkill[]> {
|
|
274
314
|
const start = await realFolder(root.dir);
|
|
315
|
+
|
|
275
316
|
if (start === null) {
|
|
276
317
|
return [];
|
|
277
318
|
}
|
|
319
|
+
|
|
278
320
|
const visited = new Set([start]);
|
|
279
321
|
const depth = root.depth ?? 1;
|
|
280
322
|
const entriesPerFolder = limits.entries ?? ENTRIES_PER_FOLDER;
|
|
281
323
|
const skills: ChatSkill[] = [];
|
|
282
324
|
let level = [root.dir];
|
|
283
325
|
let listed = 0;
|
|
326
|
+
|
|
284
327
|
for (let below = 1; below <= depth && level.length > 0; below++) {
|
|
285
328
|
const next: string[] = [];
|
|
329
|
+
|
|
286
330
|
for (const folder of level) {
|
|
287
331
|
if (listed >= limits.directories || skills.length >= limits.skills) {
|
|
288
332
|
return skills;
|
|
289
333
|
}
|
|
334
|
+
|
|
290
335
|
listed++;
|
|
291
336
|
let entries: Dirent[];
|
|
337
|
+
|
|
292
338
|
try {
|
|
293
339
|
entries = await whileBusy(() => readdir(folder, { withFileTypes: true }));
|
|
294
340
|
} catch (e) {
|
|
295
341
|
if (isBusy(e)) {
|
|
296
342
|
throw e;
|
|
297
343
|
}
|
|
344
|
+
|
|
298
345
|
continue;
|
|
299
346
|
}
|
|
347
|
+
|
|
300
348
|
// A skill manager links each skill in from a shared folder, so a link counts as much as a folder.
|
|
301
349
|
const candidates = entries
|
|
302
350
|
.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith('.'))
|
|
303
351
|
.map((entry) => ({ name: entry.name, path: join(folder, entry.name) }))
|
|
304
352
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
305
353
|
.slice(0, entriesPerFolder);
|
|
354
|
+
|
|
306
355
|
const reals = await mapLimited(candidates, SCAN_CONCURRENCY, (candidate) => realFolder(candidate.path));
|
|
356
|
+
|
|
307
357
|
const fresh = candidates.flatMap((candidate, index) => {
|
|
308
358
|
const real = reals[index];
|
|
359
|
+
|
|
309
360
|
return real === null || real === undefined || visited.has(real) ? [] : [{ ...candidate, real }];
|
|
310
361
|
});
|
|
362
|
+
|
|
311
363
|
const heads = await mapLimited(fresh, SCAN_CONCURRENCY, (candidate) => skillHead(candidate.path));
|
|
364
|
+
|
|
312
365
|
for (const [index, candidate] of fresh.entries()) {
|
|
313
366
|
// Two links in one folder can lead to the same place; the first one read it.
|
|
314
367
|
if (visited.has(candidate.real)) {
|
|
315
368
|
continue;
|
|
316
369
|
}
|
|
370
|
+
|
|
317
371
|
const head = heads[index];
|
|
372
|
+
|
|
318
373
|
if (head !== null && head !== undefined) {
|
|
319
374
|
visited.add(candidate.real);
|
|
320
375
|
const skill = skillOf(root, candidate.name, head);
|
|
376
|
+
|
|
321
377
|
if (skill !== null && skills.length < limits.skills) {
|
|
322
378
|
skills.push(skill);
|
|
323
379
|
}
|
|
@@ -327,24 +383,29 @@ async function scanRoot(root: SkillRoot, limits: SkillScanLimits): Promise<ChatS
|
|
|
327
383
|
}
|
|
328
384
|
}
|
|
329
385
|
}
|
|
386
|
+
|
|
330
387
|
level = next;
|
|
331
388
|
}
|
|
389
|
+
|
|
332
390
|
return skills;
|
|
333
391
|
}
|
|
334
392
|
|
|
335
393
|
/* Every skill under these roots, sorted by name; the first root that carries a name wins. */
|
|
336
394
|
export async function scanSkillRoots(roots: SkillRoot[], limits: SkillScanLimits = SCAN_LIMITS): Promise<ChatSkill[]> {
|
|
337
395
|
const found = new Map<string, ChatSkill>();
|
|
396
|
+
|
|
338
397
|
for (const root of roots) {
|
|
339
398
|
if (found.size >= limits.skills) {
|
|
340
399
|
break;
|
|
341
400
|
}
|
|
401
|
+
|
|
342
402
|
for (const skill of await scanRoot(root, limits)) {
|
|
343
403
|
if (!found.has(skill.name) && found.size < limits.skills) {
|
|
344
404
|
found.set(skill.name, skill);
|
|
345
405
|
}
|
|
346
406
|
}
|
|
347
407
|
}
|
|
408
|
+
|
|
348
409
|
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
349
410
|
}
|
|
350
411
|
|
|
@@ -352,7 +413,6 @@ export async function discoverSkills(kind: AgentKind, cwd: string, options: Skil
|
|
|
352
413
|
return scanSkillRoots(await skillRootsFor(kind, cwd, options));
|
|
353
414
|
}
|
|
354
415
|
|
|
355
|
-
// How long a scan stays good; a skill written mid-session shows up on the next look.
|
|
356
416
|
const CACHE_TTL_MS = 30_000;
|
|
357
417
|
|
|
358
418
|
/* The skills of one CLI in one folder, scanned at most once per half minute. There is no watcher. */
|
|
@@ -367,11 +427,14 @@ export class SkillIndex {
|
|
|
367
427
|
async list(kind: AgentKind, cwd: string): Promise<ChatSkill[]> {
|
|
368
428
|
const key = `${kind}:${cwd}`;
|
|
369
429
|
const cached = this.cache.get(key);
|
|
430
|
+
|
|
370
431
|
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
371
432
|
return cached.skills;
|
|
372
433
|
}
|
|
434
|
+
|
|
373
435
|
const skills = await discoverSkills(kind, cwd, this.options);
|
|
374
436
|
this.cache.set(key, { at: Date.now(), skills });
|
|
437
|
+
|
|
375
438
|
return skills;
|
|
376
439
|
}
|
|
377
440
|
|
|
@@ -21,16 +21,21 @@ export function backgroundLimitHandler(deps: BackgroundLimitDeps) {
|
|
|
21
21
|
if (deps.tasks.get(entry.payload.taskId)?.status !== 'open') {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
+
|
|
24
25
|
const chat = await loadChat(deps, entry.target);
|
|
26
|
+
|
|
25
27
|
if (chat === null) {
|
|
26
28
|
return;
|
|
27
29
|
}
|
|
30
|
+
|
|
28
31
|
if (chat.info.activeTurnId !== null) {
|
|
29
32
|
return 'wait';
|
|
30
33
|
}
|
|
34
|
+
|
|
31
35
|
if (runningInBackground(chat.thread.list()).length > 0) {
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
38
|
+
|
|
34
39
|
// A child whose process went since the limit started runs nothing any more; what it ran then is what the parent is told.
|
|
35
40
|
const running = (chat.info.background ?? []).map(commandLabel);
|
|
36
41
|
await deps.coordinator.outlasted(entry.target, running.length > 0 ? running : entry.payload.commands, entry.payload.restarted ? 'restart' : 'limit');
|
|
@@ -57,13 +62,15 @@ export async function restartBackgroundLimits(outbox: RestartedOutbox, now: numb
|
|
|
57
62
|
|
|
58
63
|
/* The `background-limit` entries of a host's outbox, one per task at most, as the coordinator owes and drops them. */
|
|
59
64
|
export function backgroundLimits(outbox: Pick<TaskOutbox, 'list' | 'enqueue' | 'remove'>): TaskCoordinatorDeps['limit'] {
|
|
65
|
+
const limitOf = (entry: AnyOutboxEntry, taskId: string): boolean => isBackgroundLimit(entry) && entry.payload.taskId === taskId;
|
|
66
|
+
|
|
60
67
|
return {
|
|
61
|
-
owed: (taskId) => outbox.list().some((entry) =>
|
|
68
|
+
owed: (taskId) => outbox.list().some((entry) => limitOf(entry, taskId)),
|
|
62
69
|
owe: (task, commands, at) =>
|
|
63
70
|
outbox.enqueue(task.projectId, task.childId, { kind: 'background-limit', payload: { taskId: task.id, commands: [...commands] } }, at),
|
|
64
71
|
lapse: async (taskId) => {
|
|
65
72
|
for (const entry of outbox.list()) {
|
|
66
|
-
if (
|
|
73
|
+
if (limitOf(entry, taskId)) {
|
|
67
74
|
await outbox.remove(entry.id);
|
|
68
75
|
}
|
|
69
76
|
}
|
|
@@ -65,19 +65,24 @@ export function endChildren(deps: EndChildrenDeps): EndChildren {
|
|
|
65
65
|
const end = async (nodeIds: readonly string[], reason: string): Promise<void> => {
|
|
66
66
|
await lineage.markEnded(nodeIds);
|
|
67
67
|
const ended = new Set(nodeIds);
|
|
68
|
+
|
|
68
69
|
for (const owed of outbox.list()) {
|
|
69
70
|
if (reviving.has(owed.kind) && ended.has(owed.target)) {
|
|
70
71
|
await outbox.remove(owed.id);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
74
|
+
|
|
73
75
|
await tasks.cancelOpen(ended, reason, now());
|
|
76
|
+
|
|
74
77
|
// A stopped chat that gave tasks of its own is not woken about them either.
|
|
75
78
|
for (const nodeId of nodeIds) {
|
|
76
79
|
await tasks.dropWake(nodeId);
|
|
77
80
|
}
|
|
81
|
+
|
|
78
82
|
for (const nodeId of [...nodeIds].reverse()) {
|
|
79
83
|
await deps.stop(nodeId, reason);
|
|
80
84
|
}
|
|
85
|
+
|
|
81
86
|
await deps.ended?.(nodeIds);
|
|
82
87
|
};
|
|
83
88
|
|
|
@@ -85,13 +90,17 @@ export function endChildren(deps: EndChildrenDeps): EndChildren {
|
|
|
85
90
|
owe: async (target, named) => {
|
|
86
91
|
const nodeIds = named === undefined ? lineage.descendants(target) : [...named];
|
|
87
92
|
const projectId = nodeIds.map((id) => lineage.projectOf(id)).find((id) => id !== null) ?? null;
|
|
93
|
+
|
|
88
94
|
if (nodeIds.length === 0 || projectId === null) {
|
|
89
95
|
return 0;
|
|
90
96
|
}
|
|
97
|
+
|
|
91
98
|
const owed = outbox.list().some((entry) => entry.kind === 'end-children' && entry.target === target);
|
|
99
|
+
|
|
92
100
|
if (!owed) {
|
|
93
101
|
await outbox.enqueue(projectId, target, { kind: 'end-children', payload: { nodeIds } });
|
|
94
102
|
}
|
|
103
|
+
|
|
95
104
|
return nodeIds.length;
|
|
96
105
|
},
|
|
97
106
|
handler: (entry) => end([...new Set([...entry.payload.nodeIds, ...lineage.descendants(entry.target)])], deps.reason),
|
package/src/tasks/give-task.ts
CHANGED
|
@@ -29,24 +29,32 @@ export interface GiveTaskDeps {
|
|
|
29
29
|
export function giveTaskHandler(deps: GiveTaskDeps) {
|
|
30
30
|
return async (entry: GiveTaskEntry): Promise<OutboxOutcome> => {
|
|
31
31
|
const task = deps.tasks.get(entry.payload.taskId);
|
|
32
|
+
|
|
32
33
|
// Settled or cancelled before the turn could open, or a node the project no longer places.
|
|
33
34
|
if (!task || task.status !== 'open' || !deps.placed(task.childId)) {
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
37
|
+
|
|
36
38
|
const chat = await deps.chat(task.childId);
|
|
39
|
+
|
|
37
40
|
if (chat === null) {
|
|
38
41
|
throw new Error(`${task.childId} has no chat to open a turn in`);
|
|
39
42
|
}
|
|
43
|
+
|
|
40
44
|
// The turn names the task, so a restart between opening it and dropping this entry gives it once.
|
|
41
45
|
if (chat.items().some((item) => item.kind === 'turn' && (item.taskIds ?? []).includes(task.id))) {
|
|
42
46
|
await chat.persist?.();
|
|
47
|
+
|
|
43
48
|
return;
|
|
44
49
|
}
|
|
50
|
+
|
|
45
51
|
const from = deps.titleFor(task.parentId) ?? task.parentId;
|
|
46
52
|
const text = deps.assignment?.(task) ?? task.prompt;
|
|
53
|
+
|
|
47
54
|
if (!chat.wake({ text, label: task.title, note: taskNote(from), taskIds: [task.id] })) {
|
|
48
55
|
return 'wait';
|
|
49
56
|
}
|
|
57
|
+
|
|
50
58
|
await chat.persist?.();
|
|
51
59
|
};
|
|
52
60
|
}
|