@adecore/agents 0.21.1 → 0.22.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.js +3 -8
- package/dist/chat/backend.d.ts +4 -0
- package/dist/chat/chat-core.d.ts +5 -2
- package/dist/chat/chat-core.js +8 -7
- package/dist/chat/chat-log.js +2 -2
- package/dist/chat/chat-session.d.ts +11 -2
- package/dist/chat/chat-session.js +141 -13
- package/dist/chat/chat-store.js +4 -7
- package/dist/chat/chat-title.js +4 -8
- package/dist/chat/claude-backend.d.ts +1 -0
- package/dist/chat/claude-backend.js +8 -6
- package/dist/chat/claude-protocol.d.ts +2 -0
- package/dist/chat/claude-protocol.js +21 -1
- package/dist/chat/claude-title.js +1 -1
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +1 -1
- package/dist/chat/codex-backend.d.ts +2 -2
- package/dist/chat/codex-backend.js +5 -8
- package/dist/chat/codex-protocol.js +1 -1
- package/dist/chat/codex-thread.js +4 -5
- package/dist/chat/composer-preferences.js +2 -3
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.d.ts +26 -0
- package/dist/chat/external-sessions.js +136 -0
- package/dist/chat/fake-claude.js +73 -0
- package/dist/chat/limit-resume.js +2 -7
- package/dist/chat/thread-search.d.ts +14 -0
- package/dist/chat/thread-search.js +64 -0
- package/dist/chat/visual-store.js +8 -6
- package/dist/context/refusal.js +3 -7
- package/dist/context/verb.js +1 -5
- package/dist/host/agent-host.js +2 -2
- package/dist/host/handlers.js +1 -2
- package/dist/host/wiring.js +1 -1
- package/dist/lineage.js +2 -3
- package/dist/messages/deliver-message.js +6 -11
- package/dist/messages/deliver-notice.js +8 -12
- package/dist/messages/notice-store.js +3 -5
- package/dist/outbox/outbox-worker.js +5 -2
- package/dist/providers/accounts/service.d.ts +2 -1
- package/dist/providers/accounts/service.js +11 -13
- package/dist/providers/catalog.js +2 -1
- package/dist/providers/claude.js +0 -1
- package/dist/providers/detect.js +2 -10
- package/dist/record-directory.js +0 -1
- package/dist/tasks/task-coordinator.d.ts +1 -0
- package/dist/tasks/task-coordinator.js +34 -34
- package/dist/tasks/task-store.js +1 -1
- package/dist/tasks/task-work.js +2 -2
- package/dist/tasks/waiting-child.d.ts +1 -0
- package/dist/tasks/waiting-child.js +8 -6
- package/dist/tasks/wake-parent.js +15 -10
- package/dist/tasks/wiring.js +32 -37
- package/dist/usage/aggregate.js +62 -46
- package/dist/usage/exchange.js +1 -1
- package/dist/usage/limits/monitor.d.ts +2 -2
- package/dist/usage/limits/monitor.js +2 -2
- package/dist/usage/limits/probe.js +1 -1
- package/dist/usage/pricing.js +1 -1
- package/dist/usage/readers/codex.d.ts +1 -0
- package/dist/usage/readers/codex.js +1 -1
- package/dist/usage/roots.js +1 -2
- package/dist/usage/scanner.js +7 -9
- package/dist/usage/usage-service.d.ts +1 -2
- package/dist/usage/usage-service.js +3 -6
- package/package.json +3 -3
- package/src/async.ts +3 -9
- package/src/chat/backend.ts +2 -0
- package/src/chat/chat-core.ts +8 -7
- package/src/chat/chat-log.ts +2 -2
- package/src/chat/chat-session.ts +148 -13
- package/src/chat/chat-store.ts +4 -8
- package/src/chat/chat-title.ts +4 -8
- package/src/chat/claude-backend.ts +9 -6
- package/src/chat/claude-protocol.ts +23 -1
- package/src/chat/claude-title.ts +1 -1
- package/src/chat/claude-transcript.ts +2 -2
- package/src/chat/codex-backend.ts +5 -8
- package/src/chat/codex-protocol.ts +1 -1
- package/src/chat/codex-thread.ts +6 -5
- package/src/chat/composer-preferences.ts +2 -3
- package/src/chat/errors.ts +2 -0
- package/src/chat/external-sessions.ts +177 -0
- package/src/chat/fake-claude.ts +75 -1
- package/src/chat/limit-resume.ts +2 -7
- package/src/chat/thread-search.ts +87 -0
- package/src/chat/thread.ts +1 -1
- package/src/chat/visual-store.ts +9 -6
- package/src/chat/wake-chat.ts +4 -1
- package/src/context/refusal.ts +3 -7
- package/src/context/verb.ts +1 -5
- package/src/host/agent-host.ts +2 -2
- package/src/host/handlers.ts +1 -2
- package/src/host/wiring.ts +1 -1
- package/src/lineage.ts +2 -3
- package/src/messages/deliver-message.ts +6 -11
- package/src/messages/deliver-notice.ts +8 -12
- package/src/messages/notice-store.ts +3 -5
- package/src/outbox/outbox-worker.ts +5 -2
- package/src/providers/accounts/service.ts +12 -13
- package/src/providers/accounts/variables.ts +1 -0
- package/src/providers/catalog.ts +2 -1
- package/src/providers/claude.ts +0 -1
- package/src/providers/detect.ts +2 -11
- package/src/record-directory.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/tasks/task-coordinator.ts +35 -33
- package/src/tasks/task-store.ts +1 -1
- package/src/tasks/task-work.ts +2 -2
- package/src/tasks/waiting-child.ts +10 -7
- package/src/tasks/wake-parent.ts +21 -16
- package/src/tasks/wiring.ts +33 -41
- package/src/usage/aggregate.ts +80 -50
- package/src/usage/exchange.ts +1 -1
- package/src/usage/limits/monitor.ts +4 -4
- package/src/usage/limits/probe.ts +2 -2
- package/src/usage/pricing.ts +1 -1
- package/src/usage/readers/codex.ts +1 -1
- package/src/usage/roots.ts +1 -2
- package/src/usage/scanner.ts +8 -10
- package/src/usage/usage-service.ts +5 -9
|
@@ -7,12 +7,12 @@ export interface CodexBackendOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class CodexBackend implements ChatBackend {
|
|
9
9
|
readonly acknowledgesTurns = true;
|
|
10
|
-
private promptId;
|
|
11
10
|
private readonly launch;
|
|
12
|
-
private imageWrites;
|
|
13
11
|
private readonly host;
|
|
14
12
|
private readonly protocol;
|
|
15
13
|
private readonly client;
|
|
14
|
+
private promptId;
|
|
15
|
+
private imageWrites;
|
|
16
16
|
private transport;
|
|
17
17
|
private threadId;
|
|
18
18
|
private contextPending;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
2
|
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
3
|
+
import { errorText } from '../error-text.js';
|
|
3
4
|
import { codexServiceTier, codexThreadOptions } from '../providers/codex.js';
|
|
4
5
|
import { CodexProtocol } from './codex-protocol.js';
|
|
5
|
-
import { attachmentNote } from './input.js';
|
|
6
6
|
import { CodexTransport, DEFAULT_CODEX_CLIENT } from './codex-transport.js';
|
|
7
|
-
import {
|
|
7
|
+
import { attachmentNote } from './input.js';
|
|
8
8
|
function textInput(text) {
|
|
9
9
|
return [{ type: 'text', text, text_elements: [] }];
|
|
10
10
|
}
|
|
@@ -37,18 +37,15 @@ export function parseSkillsList(result) {
|
|
|
37
37
|
*/
|
|
38
38
|
export class CodexBackend {
|
|
39
39
|
acknowledgesTurns = true;
|
|
40
|
-
promptId = null;
|
|
41
40
|
launch;
|
|
42
|
-
imageWrites = null;
|
|
43
41
|
host;
|
|
44
42
|
protocol;
|
|
45
43
|
client;
|
|
44
|
+
promptId = null;
|
|
45
|
+
imageWrites = null;
|
|
46
46
|
transport = null;
|
|
47
47
|
threadId = '';
|
|
48
|
-
|
|
49
|
-
* Codex 0.154 ignores developer instructions on `thread/resume` (measured), so a thread started
|
|
50
|
-
* before anything was linked would never hear of its links; the first prompt of a resumed process says so instead.
|
|
51
|
-
*/
|
|
48
|
+
// Codex 0.154 ignores developer instructions on `thread/resume` (measured), so the first prompt of a resumed process carries them.
|
|
52
49
|
contextPending = null;
|
|
53
50
|
imageInputSupported = null;
|
|
54
51
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -130,7 +130,7 @@ function elicitationInput(meta) {
|
|
|
130
130
|
}
|
|
131
131
|
return input;
|
|
132
132
|
}
|
|
133
|
-
//
|
|
133
|
+
// The latest reset of every spent window; one without a known reset leaves resuming to a person.
|
|
134
134
|
function spentUntil(windows) {
|
|
135
135
|
const spent = windows.filter((window) => window.used >= 1);
|
|
136
136
|
if (spent.length === 0 || spent.some((window) => window.resetsAt === null)) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { errorText } from '../error-text.js';
|
|
1
2
|
import { CodexProtocol } from './codex-protocol.js';
|
|
2
3
|
import { CodexTransport, DEFAULT_CODEX_CLIENT } from './codex-transport.js';
|
|
3
4
|
import { ChatError } from './errors.js';
|
|
4
5
|
import { readingThread, settledReading } from './subagent-projection.js';
|
|
5
|
-
import { errorText } from '../error-text.js';
|
|
6
6
|
function isRecord(value) {
|
|
7
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
8
|
}
|
|
@@ -85,14 +85,13 @@ async function nthTurnId(transport, threadId, turns) {
|
|
|
85
85
|
let seen = 0;
|
|
86
86
|
let cursor = null;
|
|
87
87
|
do {
|
|
88
|
-
const page = await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) });
|
|
89
|
-
|
|
90
|
-
for (const turn of data) {
|
|
88
|
+
const page = parseThreadItemsPage(await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) }));
|
|
89
|
+
for (const turn of page.entries) {
|
|
91
90
|
if (++seen === turns) {
|
|
92
91
|
return typeof turn.id === 'string' ? turn.id : null;
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
|
-
cursor =
|
|
94
|
+
cursor = page.nextCursor;
|
|
96
95
|
} while (cursor !== null);
|
|
97
96
|
return null;
|
|
98
97
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* What each connected client says a new chat starts with, for the chats the host starts with no
|
|
3
|
-
* client mounting them. Held per
|
|
4
|
-
*
|
|
5
|
-
* host's own defaults.
|
|
3
|
+
* client mounting them. Held per client only, so with nobody connected a chat gets the host's own
|
|
4
|
+
* defaults rather than the pick of a person who left.
|
|
6
5
|
*/
|
|
7
6
|
export class ComposerPreferences {
|
|
8
7
|
byClient = new Map();
|
package/dist/chat/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { CodedError } from '../coded-error.ts';
|
|
2
|
-
export type ChatErrorCode = 'refused-query' | 'stale-ui-query' | 'history-expired' | 'chat-not-found' | 'chat-unreadable' | 'chat-busy' | 'request-not-found' | 'chat-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'task-not-found' | 'transcript-missing' | 'transcript-format' | 'fork-failed' | 'turn-not-found' | 'item-not-found' | 'bookmark-not-found' | 'too-many-bookmarks' | 'visual-invalid' | 'visual-too-large';
|
|
2
|
+
export type ChatErrorCode = 'refused-query' | 'stale-ui-query' | 'history-expired' | 'chat-not-found' | 'chat-unreadable' | 'chat-busy' | 'request-not-found' | 'chat-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'subagent-stale' | 'task-not-found' | 'transcript-missing' | 'transcript-format' | 'fork-failed' | 'turn-not-found' | 'item-not-found' | 'bookmark-not-found' | 'too-many-bookmarks' | 'visual-invalid' | 'visual-too-large';
|
|
3
3
|
export declare class ChatError<TCode extends string = ChatErrorCode> extends CodedError<TCode> {
|
|
4
4
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SessionFs {
|
|
2
|
+
readdir(path: string): Promise<string[]>;
|
|
3
|
+
stat(path: string): Promise<{
|
|
4
|
+
mtimeMs: number;
|
|
5
|
+
} | null>;
|
|
6
|
+
readHead(path: string, maxBytes: number): Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
export interface ExternalSession {
|
|
9
|
+
provider: 'claude' | 'codex';
|
|
10
|
+
sessionId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
cwd: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function listClaudeSessions({ projectsDir, folder, fs }: {
|
|
16
|
+
projectsDir: string;
|
|
17
|
+
folder: string;
|
|
18
|
+
fs: SessionFs;
|
|
19
|
+
}): Promise<ExternalSession[]>;
|
|
20
|
+
export declare function listCodexSessions({ sessionsDir, folder, fs, maxFiles }: {
|
|
21
|
+
sessionsDir: string;
|
|
22
|
+
folder: string;
|
|
23
|
+
fs: SessionFs;
|
|
24
|
+
maxFiles?: number;
|
|
25
|
+
}): Promise<ExternalSession[]>;
|
|
26
|
+
export declare function nodeSessionFs(): Promise<SessionFs>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { clipText } from '@adecore/agent-contracts';
|
|
3
|
+
import { isForkOrSubagent } from '../usage/readers/codex.js';
|
|
4
|
+
import { claudeProjectSlug, spokenText } from './claude-transcript.js';
|
|
5
|
+
const TITLE_LENGTH = 120;
|
|
6
|
+
// Claude writes hook output and attachments ahead of the first prompt, and Codex its instructions.
|
|
7
|
+
const HEAD_BYTES = 256 * 1024;
|
|
8
|
+
const CLAUDE_MAX_FILES = 200;
|
|
9
|
+
function titleOf(text) {
|
|
10
|
+
return clipText(text.replace(/\s+/g, ' ').trim(), TITLE_LENGTH);
|
|
11
|
+
}
|
|
12
|
+
function records(head) {
|
|
13
|
+
const lines = head.split('\n');
|
|
14
|
+
// The head ends wherever the byte limit fell, so its last line may not parse.
|
|
15
|
+
return lines.flatMap((line) => {
|
|
16
|
+
if (line === '') {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(line);
|
|
21
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? [parsed] : [];
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const CLAUDE_BOOKKEEPING = new Set(['summary', 'file-history-snapshot', 'queue-operation']);
|
|
29
|
+
/* A reminder or the wrapper of a slash command is written as a user line too, but nobody said it. */
|
|
30
|
+
function claudePrompt(entry) {
|
|
31
|
+
if (CLAUDE_BOOKKEEPING.has(String(entry.type)) || entry.type !== 'user' || entry.isSidechain === true || entry.isMeta === true) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const message = typeof entry.message === 'object' && entry.message !== null ? entry.message : {};
|
|
35
|
+
const said = spokenText(message.content);
|
|
36
|
+
if (said === null || said.trim() === '' || said.startsWith('<command-') || said.startsWith('<local-command-')) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return said;
|
|
40
|
+
}
|
|
41
|
+
/*
|
|
42
|
+
* The conversations Claude Code kept for a folder, newest first. Only the folder's own directory is
|
|
43
|
+
* read, and only the `.jsonl` files in it, so the transcripts of subagents in `<session>/subagents` stay out.
|
|
44
|
+
* A file without a prompt somebody said is no conversation.
|
|
45
|
+
*/
|
|
46
|
+
export async function listClaudeSessions({ projectsDir, folder, fs }) {
|
|
47
|
+
const dir = join(projectsDir, claudeProjectSlug(folder));
|
|
48
|
+
const names = (await fs.readdir(dir)).filter((name) => name.endsWith('.jsonl'));
|
|
49
|
+
const stats = await Promise.all(names.map(async (name) => ({ name, stat: await fs.stat(join(dir, name)) })));
|
|
50
|
+
const newest = stats
|
|
51
|
+
.flatMap(({ name, stat }) => (stat === null ? [] : [{ name, mtimeMs: stat.mtimeMs }]))
|
|
52
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
53
|
+
.slice(0, CLAUDE_MAX_FILES);
|
|
54
|
+
const sessions = [];
|
|
55
|
+
for (const { name, mtimeMs } of newest) {
|
|
56
|
+
const prompt = records(await fs.readHead(join(dir, name), HEAD_BYTES))
|
|
57
|
+
.map(claudePrompt)
|
|
58
|
+
.find((said) => said !== null);
|
|
59
|
+
if (prompt !== undefined && prompt !== null) {
|
|
60
|
+
sessions.push({ provider: 'claude', sessionId: name.slice(0, -'.jsonl'.length), title: titleOf(prompt), updatedAt: mtimeMs, cwd: folder });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return sessions;
|
|
64
|
+
}
|
|
65
|
+
function codexPrompt(entry) {
|
|
66
|
+
const payload = typeof entry.payload === 'object' && entry.payload !== null ? entry.payload : null;
|
|
67
|
+
if (payload === null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (entry.type === 'event_msg' && payload.type === 'user_message' && typeof payload.message === 'string') {
|
|
71
|
+
return payload.message.trim() === '' ? null : payload.message;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/* The newest names first; the folders are named by year, month and day, and the files by when they began. */
|
|
76
|
+
function newestFirst(names) {
|
|
77
|
+
return [...names].sort().reverse();
|
|
78
|
+
}
|
|
79
|
+
async function rolloutFiles(sessionsDir, fs, max) {
|
|
80
|
+
const files = [];
|
|
81
|
+
for (const year of newestFirst(await fs.readdir(sessionsDir))) {
|
|
82
|
+
for (const month of newestFirst(await fs.readdir(join(sessionsDir, year)))) {
|
|
83
|
+
for (const day of newestFirst(await fs.readdir(join(sessionsDir, year, month)))) {
|
|
84
|
+
const names = newestFirst((await fs.readdir(join(sessionsDir, year, month, day))).filter((name) => name.startsWith('rollout-') && name.endsWith('.jsonl')));
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
files.push(join(sessionsDir, year, month, day, name));
|
|
87
|
+
if (files.length >= max) {
|
|
88
|
+
return files;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
/*
|
|
97
|
+
* The conversations Codex kept for a folder, newest first, out of the newest `maxFiles` rollouts. A
|
|
98
|
+
* rollout counts when its first line, the session's metadata, names this folder and is no fork or subagent.
|
|
99
|
+
*/
|
|
100
|
+
export async function listCodexSessions({ sessionsDir, folder, fs, maxFiles = 400 }) {
|
|
101
|
+
const sessions = [];
|
|
102
|
+
for (const path of await rolloutFiles(sessionsDir, fs, maxFiles)) {
|
|
103
|
+
const entries = records(await fs.readHead(path, HEAD_BYTES));
|
|
104
|
+
const meta = entries[0];
|
|
105
|
+
const payload = meta?.type === 'session_meta' && typeof meta.payload === 'object' && meta.payload !== null ? meta.payload : null;
|
|
106
|
+
if (payload === null || payload.cwd !== folder || typeof payload.id !== 'string' || isForkOrSubagent(payload)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const prompt = entries.map(codexPrompt).find((said) => said !== null);
|
|
110
|
+
if (prompt === undefined || prompt === null) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const stat = await fs.stat(path);
|
|
114
|
+
sessions.push({ provider: 'codex', sessionId: payload.id, title: titleOf(prompt), updatedAt: stat?.mtimeMs ?? 0, cwd: folder });
|
|
115
|
+
}
|
|
116
|
+
return sessions;
|
|
117
|
+
}
|
|
118
|
+
/* A `node:fs` behind the seam, for a host that lists the real thing. */
|
|
119
|
+
export async function nodeSessionFs() {
|
|
120
|
+
const { readdir, stat, open } = await import('node:fs/promises');
|
|
121
|
+
return {
|
|
122
|
+
readdir: async (path) => readdir(path).catch(() => []),
|
|
123
|
+
stat: async (path) => stat(path).catch(() => null),
|
|
124
|
+
readHead: async (path, maxBytes) => {
|
|
125
|
+
const handle = await open(path, 'r');
|
|
126
|
+
try {
|
|
127
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
128
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
129
|
+
return buffer.subarray(0, bytesRead).toString('utf8');
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await handle.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
package/dist/chat/fake-claude.js
CHANGED
|
@@ -137,6 +137,39 @@ export function fakeClaude(io) {
|
|
|
137
137
|
// What a background agent asked; its answer comes in after the turn that launched it, whenever a person gives one.
|
|
138
138
|
let backgroundAsk = null;
|
|
139
139
|
let slow = false;
|
|
140
|
+
// The two foreground subagents of `two subagents` that have not settled; the turn stays open until both have.
|
|
141
|
+
const openTwo = new Set();
|
|
142
|
+
const settleTwo = (name, stopped) => {
|
|
143
|
+
const toolUseId = name === 'a-one' ? 'toolu_two_a' : 'toolu_two_b';
|
|
144
|
+
if (!openTwo.delete(name)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (stopped) {
|
|
148
|
+
out({ type: 'system', subtype: 'task_updated', task_id: name, patch: { status: 'killed', end_time: 0 }, session_id: sessionId });
|
|
149
|
+
out({
|
|
150
|
+
type: 'system',
|
|
151
|
+
subtype: 'task_notification',
|
|
152
|
+
task_id: name,
|
|
153
|
+
tool_use_id: toolUseId,
|
|
154
|
+
status: 'stopped',
|
|
155
|
+
output_file: '',
|
|
156
|
+
summary: 'Stopped',
|
|
157
|
+
session_id: sessionId
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
out({
|
|
161
|
+
type: 'user',
|
|
162
|
+
message: {
|
|
163
|
+
role: 'user',
|
|
164
|
+
content: [{ type: 'tool_result', tool_use_id: toolUseId, content: stopped ? 'Stopped' : `${name} done`, is_error: stopped }]
|
|
165
|
+
},
|
|
166
|
+
session_id: sessionId
|
|
167
|
+
});
|
|
168
|
+
if (openTwo.size === 0) {
|
|
169
|
+
assistantText('summarized B');
|
|
170
|
+
result();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
140
173
|
// The rest of a background agent once its request was answered: its call's result, its report, and the turn the CLI opens about it.
|
|
141
174
|
const finishBackgroundAgent = (command, allowed, message) => {
|
|
142
175
|
const toolUseId = 'toolu_bgask';
|
|
@@ -182,6 +215,43 @@ export function fakeClaude(io) {
|
|
|
182
215
|
io.exit(1);
|
|
183
216
|
return;
|
|
184
217
|
}
|
|
218
|
+
// Two foreground subagents beside each other; the turn ends once both settled: a-two when the test runs what is put off, a-one by a `stop_task`.
|
|
219
|
+
if (text === 'two subagents') {
|
|
220
|
+
openTwo.add('a-one').add('a-two');
|
|
221
|
+
for (const [name, toolUseId] of [
|
|
222
|
+
['a-one', 'toolu_two_a'],
|
|
223
|
+
['a-two', 'toolu_two_b']
|
|
224
|
+
]) {
|
|
225
|
+
out({
|
|
226
|
+
type: 'assistant',
|
|
227
|
+
message: {
|
|
228
|
+
id: `msg_${nonce}_${++messageCounter}`,
|
|
229
|
+
model,
|
|
230
|
+
role: 'assistant',
|
|
231
|
+
content: [
|
|
232
|
+
{ type: 'tool_use', id: toolUseId, name: 'Agent', input: { description: name, subagent_type: 'general-purpose', prompt: name } }
|
|
233
|
+
],
|
|
234
|
+
usage
|
|
235
|
+
},
|
|
236
|
+
session_id: sessionId
|
|
237
|
+
});
|
|
238
|
+
out({
|
|
239
|
+
type: 'system',
|
|
240
|
+
subtype: 'task_started',
|
|
241
|
+
task_id: name,
|
|
242
|
+
tool_use_id: toolUseId,
|
|
243
|
+
description: name,
|
|
244
|
+
subagent_type: 'general-purpose',
|
|
245
|
+
is_backgrounded: false,
|
|
246
|
+
spawn_depth: 1,
|
|
247
|
+
task_type: 'local_agent',
|
|
248
|
+
prompt: name,
|
|
249
|
+
session_id: sessionId
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
io.later(() => settleTwo('a-two', false));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
185
255
|
if (text === 'crash loudly') {
|
|
186
256
|
io.err(`${'warming up\n'.repeat(2000)}Error: the fake lost its key\n at handleUser (fake-claude.ts)\n`);
|
|
187
257
|
io.exit(1);
|
|
@@ -932,6 +1002,9 @@ export function fakeClaude(io) {
|
|
|
932
1002
|
else if (frame.type === 'control_request') {
|
|
933
1003
|
const request = frame.request;
|
|
934
1004
|
out({ type: 'control_response', response: { subtype: 'success', request_id: frame.request_id, response: {} } });
|
|
1005
|
+
if (request.subtype === 'stop_task' && (request.task_id === 'a-one' || request.task_id === 'a-two')) {
|
|
1006
|
+
settleTwo(request.task_id, true);
|
|
1007
|
+
}
|
|
935
1008
|
if (request.subtype === 'interrupt' && slow) {
|
|
936
1009
|
slow = false;
|
|
937
1010
|
assistantText('[interrupted]');
|
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
export const LIMIT_RETRY_DELAYS_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
|
3
3
|
/* The last turn of a thread, when it stopped on a limit. */
|
|
4
4
|
export function limitedTurn(items) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (item.kind === 'turn') {
|
|
8
|
-
return item.state === 'error' && item.limit !== undefined ? item : null;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
5
|
+
const last = items.findLast((item) => item.kind === 'turn');
|
|
6
|
+
return last?.kind === 'turn' && last.state === 'error' && last.limit !== undefined ? last : null;
|
|
12
7
|
}
|
|
13
8
|
/* How many turns in a row, up to and including this one, stopped on a limit: the tries a person did not step in between. */
|
|
14
9
|
function limitedInARow(items, turn) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
export interface ThreadHit {
|
|
3
|
+
itemId: string;
|
|
4
|
+
author: 'person' | 'agent';
|
|
5
|
+
snippet: string;
|
|
6
|
+
matchStart: number;
|
|
7
|
+
matchLength: number;
|
|
8
|
+
createdAt: number;
|
|
9
|
+
turnId: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface ThreadSearchOptions {
|
|
12
|
+
limit: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function searchThreadItems(items: readonly ChatItem[], query: string, { limit }: ThreadSearchOptions): ThreadHit[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Characters kept on each side of the match.
|
|
2
|
+
const CONTEXT = 60;
|
|
3
|
+
// Emphasis and code markers carry no meaning in a one-line snippet, where they only read as noise.
|
|
4
|
+
function plain(text) {
|
|
5
|
+
return text.replace(/\*{1,3}|_{2,}|`+/g, '').replace(/\s+/g, ' ');
|
|
6
|
+
}
|
|
7
|
+
// Moves a cut forward to the start of the next word, never past `limit`, so a snippet does not open on half a word.
|
|
8
|
+
function wordStart(text, from, limit) {
|
|
9
|
+
if (from === 0 || /\s/.test(text[from - 1]) || /\s/.test(text[from])) {
|
|
10
|
+
return from;
|
|
11
|
+
}
|
|
12
|
+
const space = text.slice(from, limit).search(/\s/);
|
|
13
|
+
return space === -1 ? from : from + space + 1;
|
|
14
|
+
}
|
|
15
|
+
// Moves a cut back to the end of the previous word, never before `limit`.
|
|
16
|
+
function wordEnd(text, to, limit) {
|
|
17
|
+
if (to === text.length || /\s/.test(text[to - 1]) || /\s/.test(text[to])) {
|
|
18
|
+
return to;
|
|
19
|
+
}
|
|
20
|
+
const space = text.slice(limit, to).search(/\s\S*$/);
|
|
21
|
+
return space === -1 ? to : limit + space;
|
|
22
|
+
}
|
|
23
|
+
function snippetOf(text, index, length) {
|
|
24
|
+
const from = wordStart(text, Math.max(0, index - CONTEXT), index);
|
|
25
|
+
const to = wordEnd(text, Math.min(text.length, index + length + CONTEXT), index + length);
|
|
26
|
+
const before = `${from > 0 ? '…' : ''}${plain(text.slice(from, index))}`;
|
|
27
|
+
const match = plain(text.slice(index, index + length));
|
|
28
|
+
const after = `${plain(text.slice(index + length, to))}${to < text.length ? '…' : ''}`;
|
|
29
|
+
return { snippet: `${before}${match}${after}`, matchStart: before.length, matchLength: match.length };
|
|
30
|
+
}
|
|
31
|
+
/*
|
|
32
|
+
* The messages of a thread that hold `query`, oldest first, one hit per message. Only what a person
|
|
33
|
+
* and the agent said counts: what a subagent said (`parentToolUseId`) is not part of the conversation.
|
|
34
|
+
*/
|
|
35
|
+
export function searchThreadItems(items, query, { limit }) {
|
|
36
|
+
if (query === '' || limit <= 0) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
40
|
+
const hits = [];
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
if (item.kind !== 'user' && item.kind !== 'assistant') {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (item.kind === 'assistant' && item.parentToolUseId) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const found = pattern.exec(item.text);
|
|
49
|
+
if (found === null) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
hits.push({
|
|
53
|
+
itemId: item.id,
|
|
54
|
+
author: item.kind === 'user' ? 'person' : 'agent',
|
|
55
|
+
...snippetOf(item.text, found.index, found[0].length),
|
|
56
|
+
createdAt: item.createdAt,
|
|
57
|
+
turnId: item.turnId
|
|
58
|
+
});
|
|
59
|
+
if (hits.length >= limit) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return hits;
|
|
64
|
+
}
|
|
@@ -59,6 +59,12 @@ function tooLarge(bytes) {
|
|
|
59
59
|
return refuse('visual-too-large', `The page is ${mebibytes(bytes)} and a visual may be at most ${mebibytes(VISUAL_LIMITS.bytes)}; ` +
|
|
60
60
|
'load libraries from a public CDN URL and refer to large images by their http(s) URL instead of inlining them');
|
|
61
61
|
}
|
|
62
|
+
function refuseOversized(html) {
|
|
63
|
+
const bytes = Buffer.byteLength(html, 'utf8');
|
|
64
|
+
if (bytes > VISUAL_LIMITS.bytes) {
|
|
65
|
+
tooLarge(bytes);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
62
68
|
/*
|
|
63
69
|
* The visuals of every chat. The list of a chat is one file beside its record under `<home>/chats`;
|
|
64
70
|
* each page is a file in the chat's attachment folder, `<id>.html`, so whatever serves an attachment
|
|
@@ -104,9 +110,7 @@ export class VisualStore {
|
|
|
104
110
|
if (html.trim() === '') {
|
|
105
111
|
refuse('visual-invalid', 'The page is empty; pass one self-contained HTML document');
|
|
106
112
|
}
|
|
107
|
-
|
|
108
|
-
tooLarge(Buffer.byteLength(html, 'utf8'));
|
|
109
|
-
}
|
|
113
|
+
refuseOversized(html);
|
|
110
114
|
return this.writeWorkspaceFile(chatId, name, html);
|
|
111
115
|
}
|
|
112
116
|
writePreview(chatId, png) {
|
|
@@ -116,9 +120,7 @@ export class VisualStore {
|
|
|
116
120
|
async publish(chatId, input) {
|
|
117
121
|
const { title, html, maxHeight, layout, heights } = checked(input);
|
|
118
122
|
// Before the bootstrap is added, so a page far too large is never copied first.
|
|
119
|
-
|
|
120
|
-
tooLarge(Buffer.byteLength(html, 'utf8'));
|
|
121
|
-
}
|
|
123
|
+
refuseOversized(html);
|
|
122
124
|
const page = Buffer.from(injectVisualBootstrap(html), 'utf8');
|
|
123
125
|
if (page.byteLength > VISUAL_LIMITS.bytes) {
|
|
124
126
|
tooLarge(page.byteLength);
|
package/dist/context/refusal.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* How every no reads: `refused`, the code a script branches on and the sentence a model reads, and
|
|
3
|
-
* under it a line per thing the actor can do instead.
|
|
4
|
-
*
|
|
5
|
-
* two, and everything under the first row is read as advice.
|
|
3
|
+
* under it a line per thing the actor can do instead. A line keeps its tabs but loses its newlines,
|
|
4
|
+
* since everything under the first row is read as advice.
|
|
6
5
|
*/
|
|
7
6
|
/* A field that goes into a tab-separated line; a tab or a newline in a title would split the row. */
|
|
8
7
|
export function field(value) {
|
|
@@ -19,10 +18,7 @@ export function refusalBody(code, message, lines = []) {
|
|
|
19
18
|
const [first, ...rest] = rows(code, message, lines);
|
|
20
19
|
return [`refused\t${first}`, ...rest].join('\n');
|
|
21
20
|
}
|
|
22
|
-
/*
|
|
23
|
-
* The same without the word that names it, for a host that adds the prefix in its CLI:
|
|
24
|
-
* The context CLI puts the prefix on, so a CLI of an older build still prints one refusal.
|
|
25
|
-
*/
|
|
21
|
+
/* Without the `refused` prefix, for a host whose CLI adds it, so a CLI of an older build still prints one refusal. */
|
|
26
22
|
export function refusalRows(code, message, lines = []) {
|
|
27
23
|
return rows(code, message, lines).join('\n');
|
|
28
24
|
}
|
package/dist/context/verb.js
CHANGED
|
@@ -223,11 +223,7 @@ export function lengthOf(input) {
|
|
|
223
223
|
export function requiredField(needs) {
|
|
224
224
|
return z.string({ error: needs }).min(1, needs);
|
|
225
225
|
}
|
|
226
|
-
/*
|
|
227
|
-
* What a refusal lists, or one line saying the set is empty. A refusal that promises the groups of a
|
|
228
|
-
* canvas and then prints nothing reads as a daemon that lost them, where the truth is that there are
|
|
229
|
-
* none, which is a different thing to do something about.
|
|
230
|
-
*/
|
|
226
|
+
/* A refusal that promises a list and prints nothing reads as a host that lost it, where there is none. */
|
|
231
227
|
export function orNote(lines, note) {
|
|
232
228
|
return lines.length === 0 ? [`note\t${note}`] : lines;
|
|
233
229
|
}
|
package/dist/host/agent-host.js
CHANGED
|
@@ -10,8 +10,8 @@ function isAgentRequest(type) {
|
|
|
10
10
|
}
|
|
11
11
|
/*
|
|
12
12
|
* The chats of an app that has no daemon: it answers the agent requests over any `FramePort` and
|
|
13
|
-
* sends the agent events back on it, frames checked on arrival. One port is one client.
|
|
14
|
-
* goes on at the next start through its CLI's own session.
|
|
13
|
+
* sends the agent events back on it, frames checked on arrival. One port is one client. A chat
|
|
14
|
+
* closed with the host goes on at the next start through its CLI's own session.
|
|
15
15
|
*/
|
|
16
16
|
export class AgentHost {
|
|
17
17
|
chats;
|
package/dist/host/handlers.js
CHANGED
|
@@ -31,7 +31,7 @@ export function chatHandlers(chats, providers) {
|
|
|
31
31
|
chats.detach(payload.chatId, clientId);
|
|
32
32
|
return {};
|
|
33
33
|
},
|
|
34
|
-
'chat.send': (payload) => chats.send(payload.chatId, payload.text, { delivery: payload.delivery, mentions: payload.mentions, skills: payload.skills, chats: payload.chats }, payload.attachments),
|
|
34
|
+
'chat.send': (payload) => chats.send(payload.chatId, payload.text, { delivery: payload.delivery, mentions: payload.mentions, skills: payload.skills, chats: payload.chats, quoteSource: payload.quoteSource }, payload.attachments),
|
|
35
35
|
'chat.uiChoice': (payload, clientId) => chats.choose(payload, clientId),
|
|
36
36
|
'ui.link': (payload, clientId) => chats.linkUi(payload, clientId),
|
|
37
37
|
'ui.query': (payload, clientId) => chats.queryUi(payload, clientId),
|
|
@@ -81,7 +81,6 @@ export function chatHandlers(chats, providers) {
|
|
|
81
81
|
'chat.list': () => ({ chats: chats.list() })
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
-
/* The accounts are the person's settings of this host. */
|
|
85
84
|
export function accountHandlers(accounts) {
|
|
86
85
|
return {
|
|
87
86
|
'accounts.list': () => accounts.snapshot(),
|
package/dist/host/wiring.js
CHANGED
|
@@ -6,10 +6,10 @@ import { suggestChatTitle } from '../chat/chat-title.js';
|
|
|
6
6
|
import { VisualStore } from '../chat/visual-store.js';
|
|
7
7
|
import { ClaudeTitleReader } from '../chat/claude-title.js';
|
|
8
8
|
import { DEFAULT_CODEX_CLIENT } from '../chat/codex-transport.js';
|
|
9
|
+
import { errorText } from '../error-text.js';
|
|
9
10
|
import { definedEnv } from '../providers/accounts/launch.js';
|
|
10
11
|
import { ProviderAccountsService } from '../providers/accounts/service.js';
|
|
11
12
|
import { createClaudeProvider } from '../providers/claude-provider.js';
|
|
12
|
-
import { errorText } from '../error-text.js';
|
|
13
13
|
import { createCodexProvider } from '../providers/codex-provider.js';
|
|
14
14
|
import { codexRulesPathIn, defaultCodexHome, installCodexRules } from '../providers/codex-rules.js';
|
|
15
15
|
import { ProviderRegistry } from '../providers/registry.js';
|
package/dist/lineage.js
CHANGED
|
@@ -25,8 +25,8 @@ function openedByAgent(entry) {
|
|
|
25
25
|
return entry.agent && entry.relation !== 'fork';
|
|
26
26
|
}
|
|
27
27
|
/*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Lives under the host's data folder, outside the agent-writable project, so an agent cannot reset its
|
|
29
|
+
* own depth; on disk, so a restart of the host does not reset it either.
|
|
30
30
|
*/
|
|
31
31
|
export class AgentLineageStore {
|
|
32
32
|
dir;
|
|
@@ -117,7 +117,6 @@ export class AgentLineageStore {
|
|
|
117
117
|
const entry = this.opened.get(nodeId);
|
|
118
118
|
return entry?.relation === 'fork' ? entry.openedBy : null;
|
|
119
119
|
}
|
|
120
|
-
/* The forks made from a chat. */
|
|
121
120
|
forksOf(nodeId) {
|
|
122
121
|
return [...this.opened.all()].filter((entry) => entry.relation === 'fork' && entry.openedBy === nodeId).map((entry) => entry.nodeId);
|
|
123
122
|
}
|
|
@@ -5,13 +5,11 @@ export const DeliverMessageWorkSchema = z.object({
|
|
|
5
5
|
payload: z.object({ from: z.string().min(1) })
|
|
6
6
|
});
|
|
7
7
|
/*
|
|
8
|
-
* Opens the turn a message earns a chat. The messages
|
|
9
|
-
*
|
|
10
|
-
* own prompt reads them through, so one message is heard once whichever of the two opens the turn.
|
|
8
|
+
* Opens the turn a message earns a chat. The messages wait in the notice store and the turn's preamble
|
|
9
|
+
* takes them (`NoticeNotes`), as a person's own prompt does, so a message is heard once either way.
|
|
11
10
|
*
|
|
12
|
-
* One attempt, never `wait
|
|
13
|
-
*
|
|
14
|
-
* make the host start turns nobody asked for long after the news was worth anything.
|
|
11
|
+
* One attempt, never `wait`: a chat already in a turn reads the message in front of its next one, and
|
|
12
|
+
* holding the entry would start turns nobody asked for long after the news mattered.
|
|
15
13
|
*/
|
|
16
14
|
export function deliverMessageHandler(deps) {
|
|
17
15
|
return async (entry) => {
|
|
@@ -27,11 +25,8 @@ export function deliverMessageHandler(deps) {
|
|
|
27
25
|
if (chat === null) {
|
|
28
26
|
return;
|
|
29
27
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* landed (`showNotices`), so anything the turn adds about it only says the same thing again;
|
|
33
|
-
* the label on the turn already names who it came from.
|
|
34
|
-
*/
|
|
28
|
+
// No note and no preamble on the thread: a person read the message as it landed (`showNotices`),
|
|
29
|
+
// and the label on the turn names who it came from.
|
|
35
30
|
chat.wake({
|
|
36
31
|
text: deps.words.prompt(waiting.length),
|
|
37
32
|
label: deps.words.label(waiting),
|