@adecore/agents 0.21.1 → 0.21.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/async.js +3 -8
- package/dist/chat/chat-core.d.ts +2 -2
- package/dist/chat/chat-core.js +4 -4
- package/dist/chat/chat-log.js +2 -2
- 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-title.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/limit-resume.js +2 -7
- 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 +0 -1
- 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/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.js +5 -3
- 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/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/chat-core.ts +4 -4
- package/src/chat/chat-log.ts +2 -2
- 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-title.ts +1 -1
- package/src/chat/claude-transcript.ts +1 -1
- 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/limit-resume.ts +2 -7
- 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 +0 -1
- 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/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 +6 -3
- 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/roots.ts +1 -2
- package/src/usage/scanner.ts +8 -10
- package/src/usage/usage-service.ts +5 -9
package/dist/usage/scanner.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { mkdir, open, readFile, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { errorText } from '../error-text.js';
|
|
3
4
|
import { isNotFound, writeAtomic } from '../fs.js';
|
|
4
5
|
import { decodeIndex, encodeIndex } from './index-file.js';
|
|
5
6
|
import { foldByKey } from './record.js';
|
|
6
7
|
import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.js';
|
|
7
8
|
import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine } from './readers/codex.js';
|
|
8
9
|
import { usageRoots } from './roots.js';
|
|
9
|
-
import { errorText } from '../error-text.js';
|
|
10
10
|
/* What a file holds from `from` to its end, as it is at the read. */
|
|
11
11
|
async function readFrom(path, from) {
|
|
12
12
|
const handle = await open(path, 'r');
|
|
@@ -53,11 +53,11 @@ async function listJsonl(dir, out = []) {
|
|
|
53
53
|
* read again next time. The Codex state is snapshotted before that tail for the same reason.
|
|
54
54
|
*/
|
|
55
55
|
function parseChunk(text, provider, from, state) {
|
|
56
|
-
let records = [];
|
|
57
56
|
const end = text.lastIndexOf('\n');
|
|
58
57
|
const whole = end === -1 ? '' : text.slice(0, end);
|
|
59
58
|
const tail = text.slice(end + 1);
|
|
60
59
|
const read = (chunk, codex) => {
|
|
60
|
+
const records = [];
|
|
61
61
|
for (const raw of chunk.split('\n')) {
|
|
62
62
|
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
63
63
|
if (line === '') {
|
|
@@ -79,14 +79,12 @@ function parseChunk(text, provider, from, state) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
return records;
|
|
82
83
|
};
|
|
83
84
|
const codex = provider === 'codex' ? (state ?? createCodexState()) : null;
|
|
84
|
-
read(whole, codex);
|
|
85
|
+
const counted = read(whole, codex);
|
|
85
86
|
const committed = codex === null ? null : cloneCodexState(codex);
|
|
86
|
-
|
|
87
|
-
records = [];
|
|
88
|
-
read(tail, codex);
|
|
89
|
-
return { records: counted, tail: records, offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
|
|
87
|
+
return { records: counted, tail: read(tail, codex), offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
|
|
90
88
|
}
|
|
91
89
|
/*
|
|
92
90
|
* The account a record of a root belongs to, as a record keeps it (absent for the default account):
|
|
@@ -113,8 +111,8 @@ function withAccount(records, accountOf) {
|
|
|
113
111
|
});
|
|
114
112
|
}
|
|
115
113
|
/*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
114
|
+
* Reads only what a transcript appended since the last scan, and starts a file over once it was
|
|
115
|
+
* rewritten or cut. A deleted transcript keeps its totals, since its usage still happened.
|
|
118
116
|
*/
|
|
119
117
|
export class UsageScanner {
|
|
120
118
|
home;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { UsageAccount, UsageSummaryPayload, UsageSummaryResult } from '@adecore/agent-contracts';
|
|
2
2
|
import type { AgentSink } from '../events.ts';
|
|
3
3
|
import type { KnownProject } from './projects.ts';
|
|
4
|
-
import { type UsageScannerOptions } from './scanner.ts';
|
|
5
4
|
import type { UsageRootPath } from './roots.ts';
|
|
5
|
+
import { type UsageScannerOptions } from './scanner.ts';
|
|
6
6
|
export interface UsageServiceOptions {
|
|
7
7
|
home: string;
|
|
8
8
|
allowPriceFetch?: boolean;
|
|
@@ -31,5 +31,4 @@ export declare class UsageService {
|
|
|
31
31
|
stop(): void;
|
|
32
32
|
private rescan;
|
|
33
33
|
private runScan;
|
|
34
|
-
private emit;
|
|
35
34
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { ClientSinks } from '../client-sinks.js';
|
|
2
|
+
import { errorText } from '../error-text.js';
|
|
1
3
|
import { accountOfRecord, aggregate } from './aggregate.js';
|
|
2
4
|
import { ExchangeRates } from './exchange.js';
|
|
3
5
|
import { PriceBook } from './pricing.js';
|
|
4
6
|
import { UsageScanner } from './scanner.js';
|
|
5
|
-
import { errorText } from '../error-text.js';
|
|
6
|
-
import { ClientSinks } from '../client-sinks.js';
|
|
7
7
|
/* A scan this fresh answers the question the page is asking, so nothing is opened for it. */
|
|
8
8
|
const SCAN_TTL_MS = 60_000;
|
|
9
9
|
/* The accounts of the machine, and after them any a record names that the machine no longer has, under its id. */
|
|
@@ -121,10 +121,7 @@ export class UsageService {
|
|
|
121
121
|
}
|
|
122
122
|
this.report = report;
|
|
123
123
|
if (report.changedFiles > 0) {
|
|
124
|
-
this.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
|
|
124
|
+
this.sinks.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
emit(event) {
|
|
128
|
-
this.sinks.emit(event);
|
|
129
|
-
}
|
|
130
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"test": "bun --conditions=source test"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@adecore/agent-contracts": "0.21.
|
|
24
|
-
"@adecore/intelligent-ui": "0.21.
|
|
23
|
+
"@adecore/agent-contracts": "0.21.2",
|
|
24
|
+
"@adecore/intelligent-ui": "0.21.2",
|
|
25
25
|
"zod": "^4.6.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
package/src/async.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
/* The two waits the host does: a pause it can be woken from, and a deadline on somebody else's promise. */
|
|
2
|
-
|
|
3
1
|
/*
|
|
4
|
-
* Resolves after `ms`, or at once when the signal is raised. An abort
|
|
5
|
-
*
|
|
6
|
-
* rejection would only be caught and thrown away.
|
|
2
|
+
* Resolves after `ms`, or at once when the signal is raised. An abort resolves instead of rejecting:
|
|
3
|
+
* every caller is a loop that checks the signal itself on its next turn.
|
|
7
4
|
*/
|
|
8
5
|
export function wait(ms: number, signal?: AbortSignal): Promise<void> {
|
|
9
6
|
return new Promise((resolve) => {
|
|
@@ -19,10 +16,7 @@ export function wait(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
/*
|
|
23
|
-
* The promise, or `message` when it takes longer than `ms`. The timer is cleared either way, so a
|
|
24
|
-
* call that answers at once does not hold the process for the rest of its deadline.
|
|
25
|
-
*/
|
|
19
|
+
/* The timer is cleared either way, so a call that answers at once does not hold the process until its deadline. */
|
|
26
20
|
export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
27
21
|
return new Promise((resolve, reject) => {
|
|
28
22
|
const timer = setTimeout(() => reject(new Error(message)), ms);
|
package/src/chat/chat-core.ts
CHANGED
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
type ModelSelection,
|
|
28
28
|
type RuntimeMode
|
|
29
29
|
} from '@adecore/agent-contracts';
|
|
30
|
-
import { ChatUiQueries, type ChatUiHost } from './ui-queries.ts';
|
|
31
30
|
import { ClientSinks } from '../client-sinks.ts';
|
|
32
31
|
import { errorText } from '../error-text.ts';
|
|
33
32
|
import type { AgentEvent, AgentSink } from '../events.ts';
|
|
@@ -38,6 +37,7 @@ import { SkillIndex } from '../skills.ts';
|
|
|
38
37
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
39
38
|
import { usageRoots } from '../usage/roots.ts';
|
|
40
39
|
import type { AttachmentStore } from './attachment-store.ts';
|
|
40
|
+
import { commandLabel, isBackgroundWork, runsInBackground, type BackgroundWork } from './background-work.ts';
|
|
41
41
|
import type { BookmarkStore } from './bookmark-store.ts';
|
|
42
42
|
import { ChatLog, COMPACT_ABOVE_BYTES } from './chat-log.ts';
|
|
43
43
|
import type { SpawnChatProcess } from './chat-process.ts';
|
|
@@ -57,10 +57,10 @@ import type { CodexProcessSpec } from './codex-thread.ts';
|
|
|
57
57
|
import type { CodexClientInfo } from './codex-transport.ts';
|
|
58
58
|
import { ComposerPreferences } from './composer-preferences.ts';
|
|
59
59
|
import { DeltaCoalescer } from './delta-coalescer.ts';
|
|
60
|
-
import { readableAssistantText } from './readable-text.ts';
|
|
61
60
|
import { ChatError } from './errors.ts';
|
|
62
|
-
import {
|
|
61
|
+
import { readableAssistantText } from './readable-text.ts';
|
|
63
62
|
import { SubagentReader, type SubagentReaderOptions } from './subagent-reader.ts';
|
|
63
|
+
import { ChatUiQueries, type ChatUiHost } from './ui-queries.ts';
|
|
64
64
|
import type { VisualInput, VisualStore } from './visual-store.ts';
|
|
65
65
|
|
|
66
66
|
/* A turn that was running when the host went down, and the attempt that would take it up again. */
|
|
@@ -72,12 +72,12 @@ export interface InterruptedRun {
|
|
|
72
72
|
|
|
73
73
|
type BookmarkableItem = Extract<ChatItem, { kind: 'user' | 'assistant' }>;
|
|
74
74
|
|
|
75
|
-
/* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
|
|
76
75
|
function withoutHidden(info: ChatInfo): ChatInfo {
|
|
77
76
|
const { hidden: _hidden, ...rest } = info;
|
|
78
77
|
return rest;
|
|
79
78
|
}
|
|
80
79
|
|
|
80
|
+
/* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
|
|
81
81
|
function isBookmarkable(item: ChatItem): item is BookmarkableItem {
|
|
82
82
|
return item.kind === 'user' || (item.kind === 'assistant' && (item.parentToolUseId ?? null) === null);
|
|
83
83
|
}
|
package/src/chat/chat-log.ts
CHANGED
|
@@ -48,8 +48,8 @@ export interface ChatLogState {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/*
|
|
51
|
-
*
|
|
52
|
-
* one order. Recent lines stay in memory
|
|
51
|
+
* Numbers every broadcast event and appends it synchronously, so disk, memory and snapshot boundaries
|
|
52
|
+
* keep one order. Recent lines stay in memory to answer an attach with `since` at once.
|
|
53
53
|
*/
|
|
54
54
|
export class ChatLog {
|
|
55
55
|
private readonly path: string | null;
|
package/src/chat/chat-store.ts
CHANGED
|
@@ -4,12 +4,12 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { ChatInfoSchema, ChatItemSchema, type ChatInfo, type ChatItem } from '@adecore/agent-contracts';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { recordFileName } from '../record-directory.ts';
|
|
8
8
|
import { migrateInlineAttachments, type AttachmentStore } from './attachment-store.ts';
|
|
9
|
+
import { isBookmarkFileName } from './bookmark-store.ts';
|
|
9
10
|
import { parseLog, type ChatLogLine } from './chat-log.ts';
|
|
10
11
|
import { ChatThread } from './thread.ts';
|
|
11
12
|
import { isVisualFileName } from './visual-store.ts';
|
|
12
|
-
import { recordFileName } from '../record-directory.ts';
|
|
13
13
|
|
|
14
14
|
// zod strips what it does not know inside the info and the items, so a file written by an older build
|
|
15
15
|
// (with `interactionMode`, say) still parses and loses only the dropped field. A record without `seq`
|
|
@@ -30,7 +30,7 @@ const OWN_KEYS = new Set(['info', 'items', 'seq', 'resetSeq', 'preambles', 'uiAc
|
|
|
30
30
|
/* What a host keeps in a chat's record beside the thread; it never reaches the wire. */
|
|
31
31
|
export type ChatRecordExtras = Record<string, unknown>;
|
|
32
32
|
|
|
33
|
-
function
|
|
33
|
+
function withoutOwnKeys(record: Record<string, unknown>): ChatRecordExtras {
|
|
34
34
|
return Object.fromEntries(Object.entries(record).filter(([key]) => !OWN_KEYS.has(key)));
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -90,10 +90,6 @@ function liftUiAccess(record: unknown): unknown {
|
|
|
90
90
|
return Object.keys(uiAccess).length === 0 ? record : { ...record, uiAccess };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function withoutOwnKeys(extras: ChatRecordExtras): ChatRecordExtras {
|
|
94
|
-
return Object.fromEntries(Object.entries(extras).filter(([key]) => !OWN_KEYS.has(key)));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
93
|
export interface ChatStoreOptions {
|
|
98
94
|
attachments?: AttachmentStore | null;
|
|
99
95
|
// Files a host keeps beside a record under names that also end in .json (a chat's plans), which are no chats.
|
|
@@ -214,7 +210,7 @@ export class ChatStore {
|
|
|
214
210
|
resetSeq,
|
|
215
211
|
preambles: snapshot.preambles ?? [],
|
|
216
212
|
uiAccess: snapshot.uiAccess ?? {},
|
|
217
|
-
extras:
|
|
213
|
+
extras: withoutOwnKeys(snapshot),
|
|
218
214
|
lines
|
|
219
215
|
};
|
|
220
216
|
}
|
package/src/chat/chat-title.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentKind } from '@adecore/agent-contracts';
|
|
2
|
-
import { cleanTitle } from '../title-file.ts';
|
|
3
2
|
import type { ProviderRegistry } from '../providers/registry.ts';
|
|
4
3
|
import { runProcess } from '../run-process.ts';
|
|
4
|
+
import { cleanTitle } from '../title-file.ts';
|
|
5
5
|
|
|
6
6
|
// Enough of a conversation to name it; the rest of a long prompt or answer says nothing a title needs.
|
|
7
7
|
const MAX_PROMPT_CHARS = 2000;
|
|
@@ -19,10 +19,7 @@ function cap(text: string, limit: number): string {
|
|
|
19
19
|
return text.length <= limit ? text : `${text.slice(0, limit)}...`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/*
|
|
23
|
-
* What the CLI is asked. JSON again, as for a commit message: a model left to write freely opens
|
|
24
|
-
* with a sentence, and that sentence would become the name of the node.
|
|
25
|
-
*/
|
|
22
|
+
/* JSON, since a model left to write freely opens with a sentence, and that sentence would become the name. */
|
|
26
23
|
export function buildTitlePrompt(prompt: string, answer: string): string {
|
|
27
24
|
return [
|
|
28
25
|
'Write a short title for the conversation below, as a person would name it in a list of chats.',
|
|
@@ -42,9 +39,8 @@ export function buildTitlePrompt(prompt: string, answer: string): string {
|
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
/*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* is better than a stray line of it.
|
|
42
|
+
* Strict on purpose: a run that holds no `{"title": "..."}` object printed something other than a
|
|
43
|
+
* title, and a derived name is better than a stray line of it.
|
|
48
44
|
*/
|
|
49
45
|
export function parseTitle(output: string): string | null {
|
|
50
46
|
const start = [...output.matchAll(/\{\s*"title"/g)].at(-1)?.index;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { errorText } from '../error-text.ts';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { errorText } from '../error-text.ts';
|
|
3
3
|
import { claudeArgs, claudeEnv, promptPrefix } from '../providers/claude.ts';
|
|
4
4
|
import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
5
5
|
import { ChatChild } from './chat-process.ts';
|
|
@@ -71,12 +71,10 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
sendTurn(input: TurnInput): void {
|
|
74
|
-
// The prefix is what the CLI must see first (ultrathink), then the note about the links, then what was typed.
|
|
75
|
-
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
76
74
|
const promptId = input.promptId ?? randomUUID();
|
|
77
75
|
this.active = true;
|
|
78
76
|
this.protocol.beginPrompt(promptId);
|
|
79
|
-
this.write(
|
|
77
|
+
this.write(this.userMessage(input, promptId));
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
steerTurn(input: TurnInput): Promise<boolean> {
|
|
@@ -88,8 +86,7 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
88
86
|
this.steers.set(promptId, { resolve, reject });
|
|
89
87
|
this.protocol.beginSteer(promptId);
|
|
90
88
|
try {
|
|
91
|
-
|
|
92
|
-
this.write(buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId }));
|
|
89
|
+
this.write(this.userMessage(input, promptId));
|
|
93
90
|
} catch (error) {
|
|
94
91
|
this.steers.delete(promptId);
|
|
95
92
|
reject(error instanceof Error ? error : new Error(errorText(error)));
|
|
@@ -147,6 +144,12 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
147
144
|
return child?.end() ?? Promise.resolve();
|
|
148
145
|
}
|
|
149
146
|
|
|
147
|
+
private userMessage(input: TurnInput, promptId: string): Record<string, unknown> {
|
|
148
|
+
// What the CLI must see first (ultrathink), then the preamble, then what was typed.
|
|
149
|
+
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
150
|
+
return buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId });
|
|
151
|
+
}
|
|
152
|
+
|
|
150
153
|
private async readLines(child: ChatChild): Promise<void> {
|
|
151
154
|
const reader = child.process.stdout.getReader();
|
|
152
155
|
const decoder = new TextDecoder();
|
package/src/chat/claude-title.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { usageRoots } from '../usage/roots.ts';
|
|
5
4
|
import { cleanTitle, readLines } from '../title-file.ts';
|
|
5
|
+
import { usageRoots } from '../usage/roots.ts';
|
|
6
6
|
|
|
7
7
|
interface Progress {
|
|
8
8
|
offset: number;
|
|
@@ -5,8 +5,8 @@ import type { ChatHistoryResult, ChatItem } from '@adecore/agent-contracts';
|
|
|
5
5
|
import { readLines } from '../title-file.ts';
|
|
6
6
|
import { ClaudeProtocol } from './claude-protocol.ts';
|
|
7
7
|
import { readingThread, settledReading } from './subagent-projection.ts';
|
|
8
|
-
import type { ChatThread } from './thread.ts';
|
|
9
8
|
import type { ThreadProjector } from './projector.ts';
|
|
9
|
+
import type { ChatThread } from './thread.ts';
|
|
10
10
|
|
|
11
11
|
const CHUNK_BYTES = 4 * 1024 * 1024;
|
|
12
12
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
2
|
import { attachmentImageMime, type ChatSkill } from '@adecore/agent-contracts';
|
|
3
|
+
import { errorText } from '../error-text.ts';
|
|
3
4
|
import { codexServiceTier, codexThreadOptions } from '../providers/codex.ts';
|
|
4
5
|
import type { ApprovalDecision, BackendEvent, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
5
6
|
import { CodexProtocol } from './codex-protocol.ts';
|
|
6
|
-
import { attachmentNote } from './input.ts';
|
|
7
7
|
import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo, type CodexFrame } from './codex-transport.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { attachmentNote } from './input.ts';
|
|
9
9
|
|
|
10
10
|
function textInput(text: string) {
|
|
11
11
|
return [{ type: 'text', text, text_elements: [] }];
|
|
@@ -46,18 +46,15 @@ export interface CodexBackendOptions {
|
|
|
46
46
|
*/
|
|
47
47
|
export class CodexBackend implements ChatBackend {
|
|
48
48
|
readonly acknowledgesTurns = true;
|
|
49
|
-
private promptId: string | null = null;
|
|
50
49
|
private readonly launch: BackendLaunch;
|
|
51
|
-
private imageWrites: Promise<void> | null = null;
|
|
52
50
|
private readonly host: BackendHost;
|
|
53
51
|
private readonly protocol: CodexProtocol;
|
|
54
52
|
private readonly client: CodexClientInfo;
|
|
53
|
+
private promptId: string | null = null;
|
|
54
|
+
private imageWrites: Promise<void> | null = null;
|
|
55
55
|
private transport: CodexTransport | null = null;
|
|
56
56
|
private threadId = '';
|
|
57
|
-
|
|
58
|
-
* Codex 0.154 ignores developer instructions on `thread/resume` (measured), so a thread started
|
|
59
|
-
* before anything was linked would never hear of its links; the first prompt of a resumed process says so instead.
|
|
60
|
-
*/
|
|
57
|
+
// Codex 0.154 ignores developer instructions on `thread/resume` (measured), so the first prompt of a resumed process carries them.
|
|
61
58
|
private contextPending: string | null = null;
|
|
62
59
|
private imageInputSupported: boolean | null = null;
|
|
63
60
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -172,7 +172,7 @@ function elicitationInput(meta: Frame): Frame {
|
|
|
172
172
|
return input;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// The latest reset of every spent window; one without a known reset leaves resuming to a person.
|
|
176
176
|
function spentUntil(windows: readonly UsageWindow[]): number | null {
|
|
177
177
|
const spent = windows.filter((window) => window.used >= 1);
|
|
178
178
|
if (spent.length === 0 || spent.some((window) => window.resetsAt === null)) {
|
package/src/chat/codex-thread.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
import { errorText } from '../error-text.ts';
|
|
2
3
|
import type { SpawnChatProcess } from './chat-process.ts';
|
|
3
4
|
import { CodexProtocol } from './codex-protocol.ts';
|
|
4
5
|
import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo } from './codex-transport.ts';
|
|
5
6
|
import { ChatError } from './errors.ts';
|
|
6
7
|
import { readingThread, settledReading } from './subagent-projection.ts';
|
|
7
|
-
import { errorText } from '../error-text.ts';
|
|
8
8
|
|
|
9
9
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
10
10
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -116,14 +116,15 @@ async function nthTurnId(transport: CodexTransport, threadId: string, turns: num
|
|
|
116
116
|
let seen = 0;
|
|
117
117
|
let cursor: string | null = null;
|
|
118
118
|
do {
|
|
119
|
-
const page =
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const page = parseThreadItemsPage(
|
|
120
|
+
await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) })
|
|
121
|
+
);
|
|
122
|
+
for (const turn of page.entries) {
|
|
122
123
|
if (++seen === turns) {
|
|
123
124
|
return typeof turn.id === 'string' ? turn.id : null;
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
cursor =
|
|
127
|
+
cursor = page.nextCursor;
|
|
127
128
|
} while (cursor !== null);
|
|
128
129
|
return null;
|
|
129
130
|
}
|
|
@@ -15,9 +15,8 @@ interface Held {
|
|
|
15
15
|
|
|
16
16
|
/*
|
|
17
17
|
* What each connected client says a new chat starts with, for the chats the host starts with no
|
|
18
|
-
* client mounting them. Held per
|
|
19
|
-
*
|
|
20
|
-
* host's own defaults.
|
|
18
|
+
* client mounting them. Held per client only, so with nobody connected a chat gets the host's own
|
|
19
|
+
* defaults rather than the pick of a person who left.
|
|
21
20
|
*/
|
|
22
21
|
export class ComposerPreferences {
|
|
23
22
|
private readonly byClient = new Map<string, Held>();
|
package/src/chat/limit-resume.ts
CHANGED
|
@@ -5,13 +5,8 @@ export const LIMIT_RETRY_DELAYS_MS: readonly number[] = [60_000, 5 * 60_000, 15
|
|
|
5
5
|
|
|
6
6
|
/* The last turn of a thread, when it stopped on a limit. */
|
|
7
7
|
export function limitedTurn(items: readonly ChatItem[]): (ChatTurnItem & { limit: ChatTurnLimit }) | null {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (item.kind === 'turn') {
|
|
11
|
-
return item.state === 'error' && item.limit !== undefined ? (item as ChatTurnItem & { limit: ChatTurnLimit }) : null;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
8
|
+
const last = items.findLast((item) => item.kind === 'turn');
|
|
9
|
+
return last?.kind === 'turn' && last.state === 'error' && last.limit !== undefined ? (last as ChatTurnItem & { limit: ChatTurnLimit }) : null;
|
|
15
10
|
}
|
|
16
11
|
|
|
17
12
|
/* 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. */
|
package/src/chat/thread.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { AgentStatus, ChatApprovalItem, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
|
|
2
3
|
import { ChatError } from './errors.ts';
|
|
3
4
|
import { requestSummaries } from './request-summary.ts';
|
|
4
|
-
import type { AgentStatus, ChatApprovalItem, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
* The state of one chat as the client sees it. Every mutation answers the event that describes
|
package/src/chat/visual-store.ts
CHANGED
|
@@ -113,6 +113,13 @@ function tooLarge(bytes: number): never {
|
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function refuseOversized(html: string): void {
|
|
117
|
+
const bytes = Buffer.byteLength(html, 'utf8');
|
|
118
|
+
if (bytes > VISUAL_LIMITS.bytes) {
|
|
119
|
+
tooLarge(bytes);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
/*
|
|
117
124
|
* The visuals of every chat. The list of a chat is one file beside its record under `<home>/chats`;
|
|
118
125
|
* each page is a file in the chat's attachment folder, `<id>.html`, so whatever serves an attachment
|
|
@@ -164,9 +171,7 @@ export class VisualStore {
|
|
|
164
171
|
if (html.trim() === '') {
|
|
165
172
|
refuse('visual-invalid', 'The page is empty; pass one self-contained HTML document');
|
|
166
173
|
}
|
|
167
|
-
|
|
168
|
-
tooLarge(Buffer.byteLength(html, 'utf8'));
|
|
169
|
-
}
|
|
174
|
+
refuseOversized(html);
|
|
170
175
|
return this.writeWorkspaceFile(chatId, name, html);
|
|
171
176
|
}
|
|
172
177
|
|
|
@@ -178,9 +183,7 @@ export class VisualStore {
|
|
|
178
183
|
async publish(chatId: string, input: VisualInput): Promise<ChatVisual> {
|
|
179
184
|
const { title, html, maxHeight, layout, heights } = checked(input);
|
|
180
185
|
// Before the bootstrap is added, so a page far too large is never copied first.
|
|
181
|
-
|
|
182
|
-
tooLarge(Buffer.byteLength(html, 'utf8'));
|
|
183
|
-
}
|
|
186
|
+
refuseOversized(html);
|
|
184
187
|
const page = Buffer.from(injectVisualBootstrap(html), 'utf8');
|
|
185
188
|
if (page.byteLength > VISUAL_LIMITS.bytes) {
|
|
186
189
|
tooLarge(page.byteLength);
|
package/src/chat/wake-chat.ts
CHANGED
|
@@ -2,7 +2,10 @@ import type { ChatItem } from '@adecore/agent-contracts';
|
|
|
2
2
|
import type { ChatCore } from './chat-core.ts';
|
|
3
3
|
import type { ChatSession } from './chat-session.ts';
|
|
4
4
|
|
|
5
|
-
/*
|
|
5
|
+
/*
|
|
6
|
+
* A chat the host may open a turn in, as the outbox handlers see it. `wake` answers false while a
|
|
7
|
+
* turn or an owed resume is in the way.
|
|
8
|
+
*/
|
|
6
9
|
export interface WakeChat {
|
|
7
10
|
items(): ChatItem[];
|
|
8
11
|
persist?(): Promise<void>;
|
package/src/context/refusal.ts
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
|
|
|
8
7
|
/* A field that goes into a tab-separated line; a tab or a newline in a title would split the row. */
|
|
@@ -24,10 +23,7 @@ export function refusalBody(code: string, message: string, lines: readonly strin
|
|
|
24
23
|
return [`refused\t${first}`, ...rest].join('\n');
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
/*
|
|
28
|
-
* The same without the word that names it, for a host that adds the prefix in its CLI:
|
|
29
|
-
* The context CLI puts the prefix on, so a CLI of an older build still prints one refusal.
|
|
30
|
-
*/
|
|
26
|
+
/* Without the `refused` prefix, for a host whose CLI adds it, so a CLI of an older build still prints one refusal. */
|
|
31
27
|
export function refusalRows(code: string, message: string, lines: readonly string[] = []): string {
|
|
32
28
|
return rows(code, message, lines).join('\n');
|
|
33
29
|
}
|
package/src/context/verb.ts
CHANGED
|
@@ -382,11 +382,7 @@ export function requiredField(needs: string): z.ZodString {
|
|
|
382
382
|
return z.string({ error: needs }).min(1, needs);
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
/*
|
|
386
|
-
* What a refusal lists, or one line saying the set is empty. A refusal that promises the groups of a
|
|
387
|
-
* canvas and then prints nothing reads as a daemon that lost them, where the truth is that there are
|
|
388
|
-
* none, which is a different thing to do something about.
|
|
389
|
-
*/
|
|
385
|
+
/* A refusal that promises a list and prints nothing reads as a host that lost it, where there is none. */
|
|
390
386
|
export function orNote(lines: string[], note: string): string[] {
|
|
391
387
|
return lines.length === 0 ? [`note\t${note}`] : lines;
|
|
392
388
|
}
|
package/src/host/agent-host.ts
CHANGED
|
@@ -24,8 +24,8 @@ function isAgentRequest(type: string): type is AgentRequestType {
|
|
|
24
24
|
|
|
25
25
|
/*
|
|
26
26
|
* The chats of an app that has no daemon: it answers the agent requests over any `FramePort` and
|
|
27
|
-
* sends the agent events back on it, frames checked on arrival. One port is one client.
|
|
28
|
-
* goes on at the next start through its CLI's own session.
|
|
27
|
+
* sends the agent events back on it, frames checked on arrival. One port is one client. A chat
|
|
28
|
+
* closed with the host goes on at the next start through its CLI's own session.
|
|
29
29
|
*/
|
|
30
30
|
export class AgentHost<Core extends ChatCore = ChatCore> {
|
|
31
31
|
readonly chats: Core;
|
package/src/host/handlers.ts
CHANGED
|
@@ -116,7 +116,6 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/* The accounts are the person's settings of this host. */
|
|
120
119
|
export function accountHandlers(accounts: ProviderAccountsService): { [T in AccountRequestType]: AgentHandler<T> } {
|
|
121
120
|
return {
|
|
122
121
|
'accounts.list': () => accounts.snapshot(),
|
package/src/host/wiring.ts
CHANGED
|
@@ -9,12 +9,12 @@ import { VisualStore } from '../chat/visual-store.ts';
|
|
|
9
9
|
import type { ClaudeBackendOptions } from '../chat/claude-backend.ts';
|
|
10
10
|
import { ClaudeTitleReader } from '../chat/claude-title.ts';
|
|
11
11
|
import { DEFAULT_CODEX_CLIENT, type CodexClientInfo } from '../chat/codex-transport.ts';
|
|
12
|
+
import { errorText } from '../error-text.ts';
|
|
12
13
|
import type { AgentEvent } from '../events.ts';
|
|
13
14
|
import { definedEnv } from '../providers/accounts/launch.ts';
|
|
14
15
|
import { ProviderAccountsService } from '../providers/accounts/service.ts';
|
|
15
16
|
import type { AccountsHost } from '../providers/accounts/variables.ts';
|
|
16
17
|
import { createClaudeProvider } from '../providers/claude-provider.ts';
|
|
17
|
-
import { errorText } from '../error-text.ts';
|
|
18
18
|
import { createCodexProvider } from '../providers/codex-provider.ts';
|
|
19
19
|
import { codexRulesPathIn, defaultCodexHome, installCodexRules, type CodexRules } from '../providers/codex-rules.ts';
|
|
20
20
|
import { ProviderRegistry, type ProviderRegistryOptions } from '../providers/registry.ts';
|
package/src/lineage.ts
CHANGED
|
@@ -30,8 +30,8 @@ function openedByAgent(entry: Lineage): boolean {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* Lives under the host's data folder, outside the agent-writable project, so an agent cannot reset its
|
|
34
|
+
* own depth; on disk, so a restart of the host does not reset it either.
|
|
35
35
|
*/
|
|
36
36
|
export class AgentLineageStore {
|
|
37
37
|
readonly dir: string;
|
|
@@ -140,7 +140,6 @@ export class AgentLineageStore {
|
|
|
140
140
|
return entry?.relation === 'fork' ? entry.openedBy : null;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
/* The forks made from a chat. */
|
|
144
143
|
forksOf(nodeId: string): string[] {
|
|
145
144
|
return [...this.opened.all()].filter((entry) => entry.relation === 'fork' && entry.openedBy === nodeId).map((entry) => entry.nodeId);
|
|
146
145
|
}
|