@adecore/agents 0.25.3 → 0.25.5
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/chat/backend.d.ts +1 -0
- package/dist/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +1 -0
- package/dist/chat/chat-session.d.ts +3 -0
- package/dist/chat/chat-session.js +12 -1
- package/dist/chat/claude-backend.js +9 -2
- package/dist/chat/errors.d.ts +1 -1
- package/dist/lineage.d.ts +6 -0
- package/dist/lineage.js +7 -1
- package/dist/providers/claude-provider.js +1 -0
- package/dist/providers/claude.d.ts +1 -0
- package/dist/providers/claude.js +8 -1
- package/dist/providers/codex-provider.js +3 -0
- package/dist/providers/provider.d.ts +1 -0
- package/dist/tasks/task-coordinator.d.ts +2 -0
- package/dist/tasks/task-coordinator.js +24 -7
- package/dist/tasks/task-store.d.ts +1 -0
- package/dist/tasks/wiring.d.ts +1 -0
- package/dist/tasks/wiring.js +9 -1
- package/package.json +3 -3
- package/src/chat/backend.ts +2 -0
- package/src/chat/chat-core.ts +7 -0
- package/src/chat/chat-session.ts +17 -1
- package/src/chat/claude-backend.ts +9 -2
- package/src/chat/errors.ts +2 -0
- package/src/lineage.ts +8 -1
- package/src/providers/claude-provider.ts +1 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-provider.ts +3 -0
- package/src/providers/provider.ts +2 -0
- package/src/tasks/task-coordinator.ts +26 -7
- package/src/tasks/task-store.ts +10 -1
- package/src/tasks/wiring.ts +11 -1
package/dist/chat/backend.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface BackendLaunch {
|
|
|
14
14
|
resumeNote: string | null;
|
|
15
15
|
goal?: ChatGoal;
|
|
16
16
|
folders?: readonly string[];
|
|
17
|
+
projectConfig?: boolean;
|
|
17
18
|
spawn?: SpawnChatProcess;
|
|
18
19
|
saveGeneratedImage?(ref: string, upload: ChatAttachmentUpload): Promise<ChatAttachment>;
|
|
19
20
|
}
|
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface ChatCoreHooks {
|
|
|
30
30
|
instructionsFor?(chatId: string): string | null;
|
|
31
31
|
resumeNoteFor?(chatId: string): string | null;
|
|
32
32
|
foldersFor?(chatId: string): readonly string[];
|
|
33
|
+
projectConfigFor?(chatId: string): boolean;
|
|
33
34
|
hiddenFor?(chatId: string): boolean;
|
|
34
35
|
promptNotesFor?(chatId: string): PromptNotes | undefined;
|
|
35
36
|
referencesFor?(chatId: string): ChatReferences | undefined;
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -307,6 +307,7 @@ export class ChatCore {
|
|
|
307
307
|
instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
|
|
308
308
|
resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
|
|
309
309
|
folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
|
|
310
|
+
projectConfig: () => this.hooks.projectConfigFor?.(payload.chatId) ?? true,
|
|
310
311
|
...(promptNotes ? { promptNotes } : {}),
|
|
311
312
|
...(references ? { references } : {}),
|
|
312
313
|
...(this.checkpoints ? { checkpoints: this.checkpoints } : {}),
|
|
@@ -42,6 +42,7 @@ export interface ChatSessionOptions {
|
|
|
42
42
|
instructions?(): string | null;
|
|
43
43
|
resumeNote?(): string | null;
|
|
44
44
|
folders?(): readonly string[];
|
|
45
|
+
projectConfig?(): boolean;
|
|
45
46
|
promptNotes?: PromptNotes;
|
|
46
47
|
references?: ChatReferences;
|
|
47
48
|
checkpoints?: TurnCheckpoints;
|
|
@@ -96,6 +97,7 @@ export declare class ChatSession {
|
|
|
96
97
|
private relocating;
|
|
97
98
|
private launchedRuntimeMode;
|
|
98
99
|
private launchedFolders;
|
|
100
|
+
private launchedProjectConfig;
|
|
99
101
|
private turnReady;
|
|
100
102
|
private staleResults;
|
|
101
103
|
private spokenTurnId;
|
|
@@ -233,6 +235,7 @@ export declare class ChatSession {
|
|
|
233
235
|
private watchStart;
|
|
234
236
|
private clearStartWarning;
|
|
235
237
|
private requiresReplacement;
|
|
238
|
+
private loadsProjectConfig;
|
|
236
239
|
private hasNativeWork;
|
|
237
240
|
private processInUse;
|
|
238
241
|
private replacementWaits;
|
|
@@ -60,6 +60,7 @@ export class ChatSession {
|
|
|
60
60
|
relocating = false;
|
|
61
61
|
launchedRuntimeMode = null;
|
|
62
62
|
launchedFolders = [];
|
|
63
|
+
launchedProjectConfig = true;
|
|
63
64
|
// The checkpoint of the turn in flight; everything queued for that turn waits for it.
|
|
64
65
|
turnReady = Promise.resolve();
|
|
65
66
|
// Turns we settled ourselves whose `result` is still on its way; it may not close the turn after them.
|
|
@@ -1527,7 +1528,11 @@ export class ChatSession {
|
|
|
1527
1528
|
this.info.runtimeMode !== this.launchedRuntimeMode ||
|
|
1528
1529
|
this.info.account !== this.launchedAccount ||
|
|
1529
1530
|
this.info.cwd !== this.launchedCwd ||
|
|
1530
|
-
!sameFolders(this.options.folders?.() ?? [], this.launchedFolders)
|
|
1531
|
+
!sameFolders(this.options.folders?.() ?? [], this.launchedFolders) ||
|
|
1532
|
+
this.loadsProjectConfig() !== this.launchedProjectConfig));
|
|
1533
|
+
}
|
|
1534
|
+
loadsProjectConfig() {
|
|
1535
|
+
return this.options.projectConfig?.() !== false;
|
|
1531
1536
|
}
|
|
1532
1537
|
hasNativeWork() {
|
|
1533
1538
|
return runningInBackground(this.thread.list()).length > 0 || (this.info.background?.length ?? 0) > 0;
|
|
@@ -1551,6 +1556,10 @@ export class ChatSession {
|
|
|
1551
1556
|
if (this.backend && this.starting) {
|
|
1552
1557
|
return this.starting;
|
|
1553
1558
|
}
|
|
1559
|
+
const projectConfig = this.loadsProjectConfig();
|
|
1560
|
+
if (!projectConfig && !this.options.provider.ignoresProjectConfig) {
|
|
1561
|
+
return Promise.reject(new ChatError('project-config-unsupported', `${this.options.provider.name} cannot run without the configuration of its folder`));
|
|
1562
|
+
}
|
|
1554
1563
|
let env;
|
|
1555
1564
|
try {
|
|
1556
1565
|
env = this.options.env(this.thread.info.account);
|
|
@@ -1567,6 +1576,7 @@ export class ChatSession {
|
|
|
1567
1576
|
this.launchedCwd = info.cwd;
|
|
1568
1577
|
const folders = this.options.folders?.() ?? [];
|
|
1569
1578
|
this.launchedFolders = folders;
|
|
1579
|
+
this.launchedProjectConfig = projectConfig;
|
|
1570
1580
|
const launch = {
|
|
1571
1581
|
command: this.options.command,
|
|
1572
1582
|
cwd: info.cwd,
|
|
@@ -1581,6 +1591,7 @@ export class ChatSession {
|
|
|
1581
1591
|
resumeNote: this.options.resumeNote?.() ?? null,
|
|
1582
1592
|
...(info.goal === undefined ? {} : { goal: info.goal }),
|
|
1583
1593
|
...(folders.length > 0 ? { folders } : {}),
|
|
1594
|
+
...(projectConfig ? {} : { projectConfig }),
|
|
1584
1595
|
...(this.options.spawn ? { spawn: this.options.spawn } : {})
|
|
1585
1596
|
};
|
|
1586
1597
|
const made = { backend: null };
|
|
@@ -42,10 +42,17 @@ export class ClaudeBackend {
|
|
|
42
42
|
if (this.child) {
|
|
43
43
|
return Promise.resolve();
|
|
44
44
|
}
|
|
45
|
-
const { selection, runtimeMode, resume, folders } = this.launch;
|
|
45
|
+
const { selection, runtimeMode, resume, folders, projectConfig } = this.launch;
|
|
46
46
|
const args = [
|
|
47
47
|
...this.launch.command,
|
|
48
|
-
...claudeArgs({
|
|
48
|
+
...claudeArgs({
|
|
49
|
+
selection,
|
|
50
|
+
runtimeMode,
|
|
51
|
+
resume,
|
|
52
|
+
allowedTools: this.allowedTools,
|
|
53
|
+
...(folders ? { folders } : {}),
|
|
54
|
+
...(projectConfig === undefined ? {} : { projectConfig })
|
|
55
|
+
})
|
|
49
56
|
];
|
|
50
57
|
if (this.launch.instructions !== null) {
|
|
51
58
|
args.push('--append-system-prompt', this.launch.instructions);
|
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' | 'subagent-stale' | 'task-not-found' | 'goal-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' | 'project-config-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'subagent-stale' | 'task-not-found' | 'goal-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
|
}
|
package/dist/lineage.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ declare const LineageSchema: z.ZodObject<{
|
|
|
15
15
|
}>>;
|
|
16
16
|
createdAt: z.ZodNumber;
|
|
17
17
|
endedAt: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
peer: z.ZodOptional<z.ZodObject<{
|
|
19
|
+
machineId: z.ZodString;
|
|
20
|
+
}, z.core.$strip>>;
|
|
18
21
|
}, z.core.$strip>;
|
|
19
22
|
type Lineage = z.infer<typeof LineageSchema>;
|
|
20
23
|
export declare class AgentLineageStore {
|
|
@@ -33,6 +36,9 @@ export declare class AgentLineageStore {
|
|
|
33
36
|
openedBy: string;
|
|
34
37
|
}>;
|
|
35
38
|
ceilingOf(nodeId: string): RuntimeMode | null;
|
|
39
|
+
peerOf(nodeId: string): {
|
|
40
|
+
machineId: string;
|
|
41
|
+
} | null;
|
|
36
42
|
depthOf(nodeId: string): number;
|
|
37
43
|
openedCount(callerId: string, include?: (nodeId: string) => boolean): number;
|
|
38
44
|
madeBy(nodeId: string): string | null;
|
package/dist/lineage.js
CHANGED
|
@@ -18,7 +18,9 @@ const LineageSchema = z.object({
|
|
|
18
18
|
ceiling: RuntimeModeSchema.optional(),
|
|
19
19
|
createdAt: z.number(),
|
|
20
20
|
/* When stopping or deleting the node that opened it ended this one too; a cascade never ends a node twice. */
|
|
21
|
-
endedAt: z.number().optional()
|
|
21
|
+
endedAt: z.number().optional(),
|
|
22
|
+
/* Set when the agent that opened this one runs on another machine; `openedBy` then names it there, not a node here. */
|
|
23
|
+
peer: z.object({ machineId: z.string().min(1) }).optional()
|
|
22
24
|
});
|
|
23
25
|
/* Opened by the agent it names, which is what every rule about children is about; a fork lives on its own. */
|
|
24
26
|
function openedByAgent(entry) {
|
|
@@ -96,6 +98,10 @@ export class AgentLineageStore {
|
|
|
96
98
|
const entry = this.opened.get(nodeId);
|
|
97
99
|
return entry !== undefined && openedByAgent(entry) ? (entry.ceiling ?? null) : null;
|
|
98
100
|
}
|
|
101
|
+
/* The machine whose agent opened this node, or null for a node opened on this one. */
|
|
102
|
+
peerOf(nodeId) {
|
|
103
|
+
return this.opened.get(nodeId)?.peer ?? null;
|
|
104
|
+
}
|
|
99
105
|
/* How deep a node sits. A node nobody wrote down is one a person made, which is where a chain starts. */
|
|
100
106
|
depthOf(nodeId) {
|
|
101
107
|
return this.opened.get(nodeId)?.depth ?? 0;
|
|
@@ -10,6 +10,7 @@ export function createClaudeProvider(options = {}) {
|
|
|
10
10
|
capabilities: CLAUDE_CAPABILITIES,
|
|
11
11
|
command: ['claude'],
|
|
12
12
|
resumeCommand: CLAUDE_RESUME_COMMAND,
|
|
13
|
+
ignoresProjectConfig: true,
|
|
13
14
|
// Read from the strings of Claude Code 2.1.282; never HOME, which moves the keychain lookup along with it.
|
|
14
15
|
home: {
|
|
15
16
|
env: 'CLAUDE_CONFIG_DIR',
|
|
@@ -6,6 +6,7 @@ interface ClaudeLaunch {
|
|
|
6
6
|
resume: string | null;
|
|
7
7
|
allowedTools?: readonly string[];
|
|
8
8
|
folders?: readonly string[];
|
|
9
|
+
projectConfig?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export declare function claudeArgs(launch: ClaudeLaunch): string[];
|
|
11
12
|
export declare function claudeEnv(selection: ModelSelection): Record<string, string>;
|
package/dist/providers/claude.js
CHANGED
|
@@ -11,6 +11,12 @@ const CLAUDE_CHAT_ARGS = [
|
|
|
11
11
|
'--permission-prompt-tool',
|
|
12
12
|
'stdio'
|
|
13
13
|
];
|
|
14
|
+
/*
|
|
15
|
+
* Only the person's own settings load, and only the MCP servers a caller passes: the project and
|
|
16
|
+
* local settings files, the project's `.mcp.json` and the project's skills and commands stay out.
|
|
17
|
+
* The latter flag also drops the person's own MCP servers and connectors (measured on Claude Code 2.1.300).
|
|
18
|
+
*/
|
|
19
|
+
const CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS = ['--setting-sources=user', '--strict-mcp-config'];
|
|
14
20
|
// Effort values the CLI takes on its flag; anything else is asked for in the prompt instead.
|
|
15
21
|
const FLAG_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
16
22
|
const PERMISSION_MODE = {
|
|
@@ -30,7 +36,8 @@ export function claudeArgs(launch) {
|
|
|
30
36
|
const args = [
|
|
31
37
|
...CLAUDE_CHAT_ARGS,
|
|
32
38
|
...(launch.allowedTools ?? []).map((tool) => `--allowedTools=${tool}`),
|
|
33
|
-
...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`)
|
|
39
|
+
...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`),
|
|
40
|
+
...(launch.projectConfig === false ? CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS : [])
|
|
34
41
|
];
|
|
35
42
|
const contextWindow = launch.selection.options.contextWindow;
|
|
36
43
|
args.push('--model', `${launch.selection.model}${contextWindow === '1m' ? '[1m]' : ''}`);
|
|
@@ -11,6 +11,9 @@ export function createCodexProvider(options = {}) {
|
|
|
11
11
|
capabilities: CODEX_CAPABILITIES,
|
|
12
12
|
command: ['codex', ...CODEX_CHAT_ARGS],
|
|
13
13
|
resumeCommand: CODEX_RESUME_COMMAND,
|
|
14
|
+
// A folder's `.codex` layer loads once the person's own config trusts the folder or its repository, which a worktree inherits;
|
|
15
|
+
// neither `-c` nor `project_root_markers` turns it off, and `--disable hooks` would take the person's hooks along (Codex 0.162.1).
|
|
16
|
+
ignoresProjectConfig: false,
|
|
14
17
|
// The auth variables Codex 0.157 names in its own "no Codex credentials were found" advice.
|
|
15
18
|
home: { env: 'CODEX_HOME', fallback: '.codex', loginEnv: ['OPENAI_API_KEY', 'CODEX_API_KEY', 'CODEX_ACCESS_TOKEN'], loginCommand: 'codex login' },
|
|
16
19
|
detect: detectCli,
|
|
@@ -15,6 +15,7 @@ export interface ChatProvider {
|
|
|
15
15
|
readonly capabilities: ProviderCapabilities;
|
|
16
16
|
readonly command: string[];
|
|
17
17
|
readonly resumeCommand: string;
|
|
18
|
+
readonly ignoresProjectConfig?: boolean;
|
|
18
19
|
readonly home?: ProviderHome;
|
|
19
20
|
oneShotArgs?(prompt: string): string[];
|
|
20
21
|
readonly oneShotStdinArgs?: readonly string[];
|
|
@@ -34,6 +34,7 @@ export declare class TaskCoordinator {
|
|
|
34
34
|
constructor(deps: TaskCoordinatorDeps);
|
|
35
35
|
stop(): void;
|
|
36
36
|
settled(): Promise<void>;
|
|
37
|
+
settleFromElsewhere(taskId: string, result: TaskResult, status?: 'done' | 'failed'): Promise<Task | null>;
|
|
37
38
|
chatEvent(event: AgentEvent): void;
|
|
38
39
|
agentEnded(childId: string, text: string): void;
|
|
39
40
|
startFailed(childId: string, error: unknown): void;
|
|
@@ -41,6 +42,7 @@ export declare class TaskCoordinator {
|
|
|
41
42
|
done(childId: string, text: string): Promise<Task | null>;
|
|
42
43
|
outlasted(childId: string, commands: readonly string[], cause?: 'limit' | 'restart'): Promise<Task | null>;
|
|
43
44
|
cancelled(tasks: readonly Task[]): void;
|
|
45
|
+
private localTask;
|
|
44
46
|
private failed;
|
|
45
47
|
private answers;
|
|
46
48
|
private turnEnded;
|
|
@@ -75,6 +75,18 @@ export class TaskCoordinator {
|
|
|
75
75
|
}
|
|
76
76
|
await this.deps.tasks.settled();
|
|
77
77
|
}
|
|
78
|
+
/*
|
|
79
|
+
* Settles a task whose child runs on another machine, which no chat here observes: the answer comes
|
|
80
|
+
* from there. Wakes the parent once however often it is told; null when the task is no such task or
|
|
81
|
+
* was not open any more.
|
|
82
|
+
*/
|
|
83
|
+
async settleFromElsewhere(taskId, result, status = 'done') {
|
|
84
|
+
const task = this.deps.tasks.get(taskId);
|
|
85
|
+
if (task?.machine === undefined || task.status !== 'open') {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return this.settleNow(task, status, result);
|
|
89
|
+
}
|
|
78
90
|
chatEvent(event) {
|
|
79
91
|
if (this.stopped || event.event !== 'chat.event') {
|
|
80
92
|
return;
|
|
@@ -105,7 +117,7 @@ export class TaskCoordinator {
|
|
|
105
117
|
if (this.stopped) {
|
|
106
118
|
return;
|
|
107
119
|
}
|
|
108
|
-
const task = this.
|
|
120
|
+
const task = this.localTask(childId);
|
|
109
121
|
if (!task || !this.deps.placed(childId)) {
|
|
110
122
|
return;
|
|
111
123
|
}
|
|
@@ -120,7 +132,7 @@ export class TaskCoordinator {
|
|
|
120
132
|
}
|
|
121
133
|
/* `done` from the child itself, which wins over whatever else was about to settle it. */
|
|
122
134
|
async done(childId, text) {
|
|
123
|
-
const task = this.
|
|
135
|
+
const task = this.localTask(childId);
|
|
124
136
|
if (!task) {
|
|
125
137
|
return null;
|
|
126
138
|
}
|
|
@@ -132,7 +144,7 @@ export class TaskCoordinator {
|
|
|
132
144
|
* does when the CLI goes within one run.
|
|
133
145
|
*/
|
|
134
146
|
async outlasted(childId, commands, cause = 'limit') {
|
|
135
|
-
const task = this.
|
|
147
|
+
const task = this.localTask(childId);
|
|
136
148
|
const items = this.deps.chatItems(childId);
|
|
137
149
|
if (this.stopped || !task || items === null || this.delegating(childId)) {
|
|
138
150
|
return null;
|
|
@@ -153,9 +165,14 @@ export class TaskCoordinator {
|
|
|
153
165
|
this.recheck(task.parentId);
|
|
154
166
|
}
|
|
155
167
|
}
|
|
168
|
+
/* The open task a chat here works on; one whose child runs on another machine is settled from there. */
|
|
169
|
+
localTask(childId) {
|
|
170
|
+
const task = this.deps.tasks.openFor(childId);
|
|
171
|
+
return task?.machine === undefined ? task : undefined;
|
|
172
|
+
}
|
|
156
173
|
/* An open task of this child that nothing can carry any more. */
|
|
157
174
|
failed(childId, text) {
|
|
158
|
-
const task = this.
|
|
175
|
+
const task = this.localTask(childId);
|
|
159
176
|
if (!task || this.stopped) {
|
|
160
177
|
return;
|
|
161
178
|
}
|
|
@@ -179,7 +196,7 @@ export class TaskCoordinator {
|
|
|
179
196
|
return own === -1 ? task.requiresTaskTurn !== true && turn.createdAt >= task.createdAt : own < items.findIndex((item) => item.id === turn.id);
|
|
180
197
|
}
|
|
181
198
|
turnEnded(chatId, turn) {
|
|
182
|
-
const task = this.
|
|
199
|
+
const task = this.localTask(chatId);
|
|
183
200
|
const items = this.deps.chatItems(chatId);
|
|
184
201
|
if (!task || items === null || !this.answers(items, turn, task) || this.delegating(chatId)) {
|
|
185
202
|
return;
|
|
@@ -297,7 +314,7 @@ export class TaskCoordinator {
|
|
|
297
314
|
}
|
|
298
315
|
/* A child that took up work again is no longer waiting out a limit. */
|
|
299
316
|
turnStarted(chatId) {
|
|
300
|
-
const task = this.
|
|
317
|
+
const task = this.localTask(chatId);
|
|
301
318
|
if (task?.paused !== undefined) {
|
|
302
319
|
this.hold(task, null);
|
|
303
320
|
}
|
|
@@ -306,7 +323,7 @@ export class TaskCoordinator {
|
|
|
306
323
|
void this.deps.tasks.pause(task.id, paused).catch((e) => console.error(`Pausing task ${task.id} failed:`, errorText(e)));
|
|
307
324
|
}
|
|
308
325
|
recheck(chatId) {
|
|
309
|
-
const task = this.
|
|
326
|
+
const task = this.localTask(chatId);
|
|
310
327
|
const items = this.deps.chatItems(chatId);
|
|
311
328
|
if (!task || items === null || this.delegating(chatId)) {
|
|
312
329
|
return;
|
package/dist/tasks/wiring.d.ts
CHANGED
package/dist/tasks/wiring.js
CHANGED
|
@@ -56,7 +56,15 @@ export function wireTasks(deps) {
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
const requests = chatRequests(chats);
|
|
59
|
-
const waiting = new WaitingObserver({
|
|
59
|
+
const waiting = new WaitingObserver({
|
|
60
|
+
// A child on another machine asks its questions there.
|
|
61
|
+
openTask: (childId) => {
|
|
62
|
+
const task = deps.tasks.openFor(childId);
|
|
63
|
+
return task?.machine === undefined ? task : undefined;
|
|
64
|
+
},
|
|
65
|
+
enqueue,
|
|
66
|
+
now
|
|
67
|
+
});
|
|
60
68
|
chats.observe((event) => {
|
|
61
69
|
coordinator.chatEvent(event);
|
|
62
70
|
waiting.chatEvent(event);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.5",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"test": "bun --conditions=source test"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@adecore/agent-contracts": "0.25.
|
|
85
|
-
"@adecore/intelligent-ui": "0.25.
|
|
84
|
+
"@adecore/agent-contracts": "0.25.5",
|
|
85
|
+
"@adecore/intelligent-ui": "0.25.5",
|
|
86
86
|
"zod": "^4.6.5"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
package/src/chat/backend.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface BackendLaunch {
|
|
|
45
45
|
// Folders outside `cwd` the agent may reach without asking. Claude Code takes them as `--add-dir`; Codex's
|
|
46
46
|
// sandbox reads the whole disk already and keeps writes to the workspace, so it needs nothing.
|
|
47
47
|
folders?: readonly string[];
|
|
48
|
+
// False keeps the CLI from loading configuration out of `cwd` (settings, hooks, MCP servers); absent loads it.
|
|
49
|
+
projectConfig?: boolean;
|
|
48
50
|
// How the CLI is started; a test runs a fake in the same process, everything else spawns it.
|
|
49
51
|
spawn?: SpawnChatProcess;
|
|
50
52
|
saveGeneratedImage?(ref: string, upload: ChatAttachmentUpload): Promise<ChatAttachment>;
|
package/src/chat/chat-core.ts
CHANGED
|
@@ -125,6 +125,12 @@ export interface ChatCoreHooks {
|
|
|
125
125
|
* next turn in a new process that resumes the thread, and never cuts a turn in flight.
|
|
126
126
|
*/
|
|
127
127
|
foldersFor?(chatId: string): readonly string[];
|
|
128
|
+
/*
|
|
129
|
+
* Whether this chat's CLI loads the configuration of its working folder; false for a folder whose contents
|
|
130
|
+
* the host does not trust. Asked before every turn, like the folders. A provider with `ignoresProjectConfig`
|
|
131
|
+
* unset refuses the turn with `project-config-unsupported`, so a host can check the flag first.
|
|
132
|
+
*/
|
|
133
|
+
projectConfigFor?(chatId: string): boolean;
|
|
128
134
|
/* Whether no list of chats shows this one, which `ChatInfo.hidden` says to every client. */
|
|
129
135
|
hiddenFor?(chatId: string): boolean;
|
|
130
136
|
/* What the host says in front of a chat's next real prompt, beside what the chat keeps itself. */
|
|
@@ -490,6 +496,7 @@ export class ChatCore {
|
|
|
490
496
|
instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
|
|
491
497
|
resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
|
|
492
498
|
folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
|
|
499
|
+
projectConfig: () => this.hooks.projectConfigFor?.(payload.chatId) ?? true,
|
|
493
500
|
...(promptNotes ? { promptNotes } : {}),
|
|
494
501
|
...(references ? { references } : {}),
|
|
495
502
|
...(this.checkpoints ? { checkpoints: this.checkpoints } : {}),
|
package/src/chat/chat-session.ts
CHANGED
|
@@ -78,6 +78,8 @@ export interface ChatSessionOptions {
|
|
|
78
78
|
resumeNote?(): string | null;
|
|
79
79
|
// The folders beside the working directory, asked before every turn; a change starts the next turn in a new process.
|
|
80
80
|
folders?(): readonly string[];
|
|
81
|
+
// False runs the CLI without the configuration of the working folder; asked before every turn like the folders.
|
|
82
|
+
projectConfig?(): boolean;
|
|
81
83
|
promptNotes?: PromptNotes;
|
|
82
84
|
references?: ChatReferences;
|
|
83
85
|
// Trees per turn, so a settled turn can show what the working tree holds against its start.
|
|
@@ -184,6 +186,7 @@ export class ChatSession {
|
|
|
184
186
|
private relocating = false;
|
|
185
187
|
private launchedRuntimeMode: RuntimeMode | null = null;
|
|
186
188
|
private launchedFolders: readonly string[] = [];
|
|
189
|
+
private launchedProjectConfig = true;
|
|
187
190
|
// The checkpoint of the turn in flight; everything queued for that turn waits for it.
|
|
188
191
|
private turnReady: Promise<void> = Promise.resolve();
|
|
189
192
|
// Turns we settled ourselves whose `result` is still on its way; it may not close the turn after them.
|
|
@@ -1760,10 +1763,15 @@ export class ChatSession {
|
|
|
1760
1763
|
this.info.runtimeMode !== this.launchedRuntimeMode ||
|
|
1761
1764
|
this.info.account !== this.launchedAccount ||
|
|
1762
1765
|
this.info.cwd !== this.launchedCwd ||
|
|
1763
|
-
!sameFolders(this.options.folders?.() ?? [], this.launchedFolders)
|
|
1766
|
+
!sameFolders(this.options.folders?.() ?? [], this.launchedFolders) ||
|
|
1767
|
+
this.loadsProjectConfig() !== this.launchedProjectConfig)
|
|
1764
1768
|
);
|
|
1765
1769
|
}
|
|
1766
1770
|
|
|
1771
|
+
private loadsProjectConfig(): boolean {
|
|
1772
|
+
return this.options.projectConfig?.() !== false;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1767
1775
|
private hasNativeWork(): boolean {
|
|
1768
1776
|
return runningInBackground(this.thread.list()).length > 0 || (this.info.background?.length ?? 0) > 0;
|
|
1769
1777
|
}
|
|
@@ -1789,6 +1797,12 @@ export class ChatSession {
|
|
|
1789
1797
|
if (this.backend && this.starting) {
|
|
1790
1798
|
return this.starting;
|
|
1791
1799
|
}
|
|
1800
|
+
const projectConfig = this.loadsProjectConfig();
|
|
1801
|
+
if (!projectConfig && !this.options.provider.ignoresProjectConfig) {
|
|
1802
|
+
return Promise.reject(
|
|
1803
|
+
new ChatError('project-config-unsupported', `${this.options.provider.name} cannot run without the configuration of its folder`)
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1792
1806
|
let env: Record<string, string>;
|
|
1793
1807
|
try {
|
|
1794
1808
|
env = this.options.env(this.thread.info.account);
|
|
@@ -1804,6 +1818,7 @@ export class ChatSession {
|
|
|
1804
1818
|
this.launchedCwd = info.cwd;
|
|
1805
1819
|
const folders = this.options.folders?.() ?? [];
|
|
1806
1820
|
this.launchedFolders = folders;
|
|
1821
|
+
this.launchedProjectConfig = projectConfig;
|
|
1807
1822
|
const launch: BackendLaunch = {
|
|
1808
1823
|
command: this.options.command,
|
|
1809
1824
|
cwd: info.cwd,
|
|
@@ -1818,6 +1833,7 @@ export class ChatSession {
|
|
|
1818
1833
|
resumeNote: this.options.resumeNote?.() ?? null,
|
|
1819
1834
|
...(info.goal === undefined ? {} : { goal: info.goal }),
|
|
1820
1835
|
...(folders.length > 0 ? { folders } : {}),
|
|
1836
|
+
...(projectConfig ? {} : { projectConfig }),
|
|
1821
1837
|
...(this.options.spawn ? { spawn: this.options.spawn } : {})
|
|
1822
1838
|
};
|
|
1823
1839
|
const made: { backend: ChatBackend | null } = { backend: null };
|
|
@@ -54,10 +54,17 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
54
54
|
if (this.child) {
|
|
55
55
|
return Promise.resolve();
|
|
56
56
|
}
|
|
57
|
-
const { selection, runtimeMode, resume, folders } = this.launch;
|
|
57
|
+
const { selection, runtimeMode, resume, folders, projectConfig } = this.launch;
|
|
58
58
|
const args = [
|
|
59
59
|
...this.launch.command,
|
|
60
|
-
...claudeArgs({
|
|
60
|
+
...claudeArgs({
|
|
61
|
+
selection,
|
|
62
|
+
runtimeMode,
|
|
63
|
+
resume,
|
|
64
|
+
allowedTools: this.allowedTools,
|
|
65
|
+
...(folders ? { folders } : {}),
|
|
66
|
+
...(projectConfig === undefined ? {} : { projectConfig })
|
|
67
|
+
})
|
|
61
68
|
];
|
|
62
69
|
if (this.launch.instructions !== null) {
|
|
63
70
|
args.push('--append-system-prompt', this.launch.instructions);
|
package/src/chat/errors.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type ChatErrorCode =
|
|
|
10
10
|
| 'chat-busy'
|
|
11
11
|
| 'request-not-found'
|
|
12
12
|
| 'chat-unsupported'
|
|
13
|
+
// The host asked for a chat that ignores the configuration of its folder, and this CLI cannot.
|
|
14
|
+
| 'project-config-unsupported'
|
|
13
15
|
| 'invalid-attachments'
|
|
14
16
|
| 'subagent-not-found'
|
|
15
17
|
// The sub-agent was started by a CLI process that has ended, so no process here can stop it.
|
package/src/lineage.ts
CHANGED
|
@@ -19,7 +19,9 @@ const LineageSchema = z.object({
|
|
|
19
19
|
ceiling: RuntimeModeSchema.optional(),
|
|
20
20
|
createdAt: z.number(),
|
|
21
21
|
/* When stopping or deleting the node that opened it ended this one too; a cascade never ends a node twice. */
|
|
22
|
-
endedAt: z.number().optional()
|
|
22
|
+
endedAt: z.number().optional(),
|
|
23
|
+
/* Set when the agent that opened this one runs on another machine; `openedBy` then names it there, not a node here. */
|
|
24
|
+
peer: z.object({ machineId: z.string().min(1) }).optional()
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
type Lineage = z.infer<typeof LineageSchema>;
|
|
@@ -115,6 +117,11 @@ export class AgentLineageStore {
|
|
|
115
117
|
return entry !== undefined && openedByAgent(entry) ? (entry.ceiling ?? null) : null;
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
/* The machine whose agent opened this node, or null for a node opened on this one. */
|
|
121
|
+
peerOf(nodeId: string): { machineId: string } | null {
|
|
122
|
+
return this.opened.get(nodeId)?.peer ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
/* How deep a node sits. A node nobody wrote down is one a person made, which is where a chain starts. */
|
|
119
126
|
depthOf(nodeId: string): number {
|
|
120
127
|
return this.opened.get(nodeId)?.depth ?? 0;
|
|
@@ -12,6 +12,7 @@ export function createClaudeProvider(options: ClaudeBackendOptions = {}): ChatPr
|
|
|
12
12
|
capabilities: CLAUDE_CAPABILITIES,
|
|
13
13
|
command: ['claude'],
|
|
14
14
|
resumeCommand: CLAUDE_RESUME_COMMAND,
|
|
15
|
+
ignoresProjectConfig: true,
|
|
15
16
|
// Read from the strings of Claude Code 2.1.282; never HOME, which moves the keychain lookup along with it.
|
|
16
17
|
home: {
|
|
17
18
|
env: 'CLAUDE_CONFIG_DIR',
|
package/src/providers/claude.ts
CHANGED
|
@@ -14,6 +14,13 @@ const CLAUDE_CHAT_ARGS = [
|
|
|
14
14
|
'stdio'
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
+
/*
|
|
18
|
+
* Only the person's own settings load, and only the MCP servers a caller passes: the project and
|
|
19
|
+
* local settings files, the project's `.mcp.json` and the project's skills and commands stay out.
|
|
20
|
+
* The latter flag also drops the person's own MCP servers and connectors (measured on Claude Code 2.1.300).
|
|
21
|
+
*/
|
|
22
|
+
const CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS = ['--setting-sources=user', '--strict-mcp-config'];
|
|
23
|
+
|
|
17
24
|
// Effort values the CLI takes on its flag; anything else is asked for in the prompt instead.
|
|
18
25
|
const FLAG_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
19
26
|
|
|
@@ -37,6 +44,7 @@ interface ClaudeLaunch {
|
|
|
37
44
|
resume: string | null;
|
|
38
45
|
allowedTools?: readonly string[];
|
|
39
46
|
folders?: readonly string[];
|
|
47
|
+
projectConfig?: boolean;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export function claudeArgs(launch: ClaudeLaunch): string[] {
|
|
@@ -44,7 +52,8 @@ export function claudeArgs(launch: ClaudeLaunch): string[] {
|
|
|
44
52
|
const args = [
|
|
45
53
|
...CLAUDE_CHAT_ARGS,
|
|
46
54
|
...(launch.allowedTools ?? []).map((tool) => `--allowedTools=${tool}`),
|
|
47
|
-
...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`)
|
|
55
|
+
...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`),
|
|
56
|
+
...(launch.projectConfig === false ? CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS : [])
|
|
48
57
|
];
|
|
49
58
|
const contextWindow = launch.selection.options.contextWindow;
|
|
50
59
|
args.push('--model', `${launch.selection.model}${contextWindow === '1m' ? '[1m]' : ''}`);
|
|
@@ -13,6 +13,9 @@ export function createCodexProvider(options: CodexBackendOptions = {}): ChatProv
|
|
|
13
13
|
capabilities: CODEX_CAPABILITIES,
|
|
14
14
|
command: ['codex', ...CODEX_CHAT_ARGS],
|
|
15
15
|
resumeCommand: CODEX_RESUME_COMMAND,
|
|
16
|
+
// A folder's `.codex` layer loads once the person's own config trusts the folder or its repository, which a worktree inherits;
|
|
17
|
+
// neither `-c` nor `project_root_markers` turns it off, and `--disable hooks` would take the person's hooks along (Codex 0.162.1).
|
|
18
|
+
ignoresProjectConfig: false,
|
|
16
19
|
// The auth variables Codex 0.157 names in its own "no Codex credentials were found" advice.
|
|
17
20
|
home: { env: 'CODEX_HOME', fallback: '.codex', loginEnv: ['OPENAI_API_KEY', 'CODEX_API_KEY', 'CODEX_ACCESS_TOKEN'], loginCommand: 'codex login' },
|
|
18
21
|
detect: detectCli,
|
|
@@ -28,6 +28,8 @@ export interface ChatProvider {
|
|
|
28
28
|
readonly command: string[];
|
|
29
29
|
// What a terminal runs to continue one of this CLI's sessions; `{id}` stands for the session id.
|
|
30
30
|
readonly resumeCommand: string;
|
|
31
|
+
/* Whether `BackendLaunch.projectConfig: false` keeps this CLI from loading configuration out of the working folder; absent is no. */
|
|
32
|
+
readonly ignoresProjectConfig?: boolean;
|
|
31
33
|
/* Where this CLI keeps its login, settings and transcripts, which is what an account is. Absent on a
|
|
32
34
|
CLI with no variable to move that folder, which then only has its default account. */
|
|
33
35
|
readonly home?: ProviderHome;
|
|
@@ -126,6 +126,19 @@ export class TaskCoordinator {
|
|
|
126
126
|
await this.deps.tasks.settled();
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/*
|
|
130
|
+
* Settles a task whose child runs on another machine, which no chat here observes: the answer comes
|
|
131
|
+
* from there. Wakes the parent once however often it is told; null when the task is no such task or
|
|
132
|
+
* was not open any more.
|
|
133
|
+
*/
|
|
134
|
+
async settleFromElsewhere(taskId: string, result: TaskResult, status: 'done' | 'failed' = 'done'): Promise<Task | null> {
|
|
135
|
+
const task = this.deps.tasks.get(taskId);
|
|
136
|
+
if (task?.machine === undefined || task.status !== 'open') {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return this.settleNow(task, status, result);
|
|
140
|
+
}
|
|
141
|
+
|
|
129
142
|
chatEvent(event: AgentEvent): void {
|
|
130
143
|
if (this.stopped || event.event !== 'chat.event') {
|
|
131
144
|
return;
|
|
@@ -154,7 +167,7 @@ export class TaskCoordinator {
|
|
|
154
167
|
if (this.stopped) {
|
|
155
168
|
return;
|
|
156
169
|
}
|
|
157
|
-
const task = this.
|
|
170
|
+
const task = this.localTask(childId);
|
|
158
171
|
if (!task || !this.deps.placed(childId)) {
|
|
159
172
|
return;
|
|
160
173
|
}
|
|
@@ -172,7 +185,7 @@ export class TaskCoordinator {
|
|
|
172
185
|
|
|
173
186
|
/* `done` from the child itself, which wins over whatever else was about to settle it. */
|
|
174
187
|
async done(childId: string, text: string): Promise<Task | null> {
|
|
175
|
-
const task = this.
|
|
188
|
+
const task = this.localTask(childId);
|
|
176
189
|
if (!task) {
|
|
177
190
|
return null;
|
|
178
191
|
}
|
|
@@ -185,7 +198,7 @@ export class TaskCoordinator {
|
|
|
185
198
|
* does when the CLI goes within one run.
|
|
186
199
|
*/
|
|
187
200
|
async outlasted(childId: string, commands: readonly string[], cause: 'limit' | 'restart' = 'limit'): Promise<Task | null> {
|
|
188
|
-
const task = this.
|
|
201
|
+
const task = this.localTask(childId);
|
|
189
202
|
const items = this.deps.chatItems(childId);
|
|
190
203
|
if (this.stopped || !task || items === null || this.delegating(childId)) {
|
|
191
204
|
return null;
|
|
@@ -208,9 +221,15 @@ export class TaskCoordinator {
|
|
|
208
221
|
}
|
|
209
222
|
}
|
|
210
223
|
|
|
224
|
+
/* The open task a chat here works on; one whose child runs on another machine is settled from there. */
|
|
225
|
+
private localTask(childId: string): Task | undefined {
|
|
226
|
+
const task = this.deps.tasks.openFor(childId);
|
|
227
|
+
return task?.machine === undefined ? task : undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
211
230
|
/* An open task of this child that nothing can carry any more. */
|
|
212
231
|
private failed(childId: string, text: string): void {
|
|
213
|
-
const task = this.
|
|
232
|
+
const task = this.localTask(childId);
|
|
214
233
|
if (!task || this.stopped) {
|
|
215
234
|
return;
|
|
216
235
|
}
|
|
@@ -236,7 +255,7 @@ export class TaskCoordinator {
|
|
|
236
255
|
}
|
|
237
256
|
|
|
238
257
|
private turnEnded(chatId: string, turn: ChatTurnItem): void {
|
|
239
|
-
const task = this.
|
|
258
|
+
const task = this.localTask(chatId);
|
|
240
259
|
const items = this.deps.chatItems(chatId);
|
|
241
260
|
if (!task || items === null || !this.answers(items, turn, task) || this.delegating(chatId)) {
|
|
242
261
|
return;
|
|
@@ -360,7 +379,7 @@ export class TaskCoordinator {
|
|
|
360
379
|
|
|
361
380
|
/* A child that took up work again is no longer waiting out a limit. */
|
|
362
381
|
private turnStarted(chatId: string): void {
|
|
363
|
-
const task = this.
|
|
382
|
+
const task = this.localTask(chatId);
|
|
364
383
|
if (task?.paused !== undefined) {
|
|
365
384
|
this.hold(task, null);
|
|
366
385
|
}
|
|
@@ -371,7 +390,7 @@ export class TaskCoordinator {
|
|
|
371
390
|
}
|
|
372
391
|
|
|
373
392
|
private recheck(chatId: string): void {
|
|
374
|
-
const task = this.
|
|
393
|
+
const task = this.localTask(chatId);
|
|
375
394
|
const items = this.deps.chatItems(chatId);
|
|
376
395
|
if (!task || items === null || this.delegating(chatId)) {
|
|
377
396
|
return;
|
package/src/tasks/task-store.ts
CHANGED
|
@@ -51,7 +51,16 @@ export class TaskStore {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async open(
|
|
54
|
-
record: {
|
|
54
|
+
record: {
|
|
55
|
+
projectId: string;
|
|
56
|
+
parentId: string;
|
|
57
|
+
childId: string;
|
|
58
|
+
title: string;
|
|
59
|
+
prompt: string;
|
|
60
|
+
batchId?: string;
|
|
61
|
+
requiresTaskTurn?: boolean;
|
|
62
|
+
machine?: string;
|
|
63
|
+
},
|
|
55
64
|
now: number
|
|
56
65
|
): Promise<Task> {
|
|
57
66
|
const task: Task = {
|
package/src/tasks/wiring.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface TaskRecord {
|
|
|
22
22
|
childId: string;
|
|
23
23
|
title: string;
|
|
24
24
|
prompt: string;
|
|
25
|
+
/* The machine the child runs on, when it is not this one. */
|
|
26
|
+
machine?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/* What the verbs of a context CLI reach of the tasks. */
|
|
@@ -127,7 +129,15 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
|
|
|
127
129
|
});
|
|
128
130
|
|
|
129
131
|
const requests = chatRequests(chats);
|
|
130
|
-
const waiting = new WaitingObserver({
|
|
132
|
+
const waiting = new WaitingObserver({
|
|
133
|
+
// A child on another machine asks its questions there.
|
|
134
|
+
openTask: (childId) => {
|
|
135
|
+
const task = deps.tasks.openFor(childId);
|
|
136
|
+
return task?.machine === undefined ? task : undefined;
|
|
137
|
+
},
|
|
138
|
+
enqueue,
|
|
139
|
+
now
|
|
140
|
+
});
|
|
131
141
|
|
|
132
142
|
chats.observe((event) => {
|
|
133
143
|
coordinator.chatEvent(event);
|