@canonmsg/codex-plugin 0.19.0 → 0.19.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/adapter.d.ts +1 -0
- package/dist/app-server-adapter.d.ts +10 -0
- package/dist/app-server-adapter.js +53 -4
- package/dist/host.js +64 -8
- package/dist/turn-activity.d.ts +19 -0
- package/dist/turn-activity.js +18 -0
- package/package.json +7 -7
package/dist/adapter.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare class CodexAppServerAdapter {
|
|
|
39
39
|
private skillsCache;
|
|
40
40
|
private messageTextByItem;
|
|
41
41
|
private planText;
|
|
42
|
+
private traceTurnEpochMs;
|
|
42
43
|
constructor(opts: {
|
|
43
44
|
cwd: string;
|
|
44
45
|
threadId?: string | null;
|
|
@@ -84,9 +85,18 @@ export declare class CodexAppServerAdapter {
|
|
|
84
85
|
private handleLine;
|
|
85
86
|
private handleServerRequest;
|
|
86
87
|
private handleNotification;
|
|
88
|
+
/**
|
|
89
|
+
* The turn's final message is the JOIN of every agentMessage item, in
|
|
90
|
+
* insertion order — not just the last item. The streamed turn trail folds
|
|
91
|
+
* all items (one text segment per item), so the final text must match the
|
|
92
|
+
* streamed/folded text or the app renders the turn twice.
|
|
93
|
+
*/
|
|
94
|
+
private joinedAgentMessageText;
|
|
87
95
|
private isCurrentThreadNotification;
|
|
88
96
|
private resolveCurrentTurn;
|
|
89
97
|
private clearActiveTurn;
|
|
90
98
|
private sendRequest;
|
|
91
99
|
private write;
|
|
100
|
+
private trace;
|
|
101
|
+
private traceLine;
|
|
92
102
|
}
|
|
@@ -31,6 +31,7 @@ export class CodexAppServerAdapter {
|
|
|
31
31
|
skillsCache = null;
|
|
32
32
|
messageTextByItem = new Map();
|
|
33
33
|
planText = '';
|
|
34
|
+
traceTurnEpochMs = null;
|
|
34
35
|
constructor(opts) {
|
|
35
36
|
this.cwd = opts.cwd;
|
|
36
37
|
this.threadId = opts.threadId ?? null;
|
|
@@ -117,6 +118,8 @@ export class CodexAppServerAdapter {
|
|
|
117
118
|
this.interrupted = false;
|
|
118
119
|
this.messageTextByItem.clear();
|
|
119
120
|
this.planText = '';
|
|
121
|
+
this.traceTurnEpochMs = Date.now();
|
|
122
|
+
this.trace('turn/run begin');
|
|
120
123
|
try {
|
|
121
124
|
if (this.threadId && this.loadedThreadId !== this.threadId) {
|
|
122
125
|
const resumed = await this.sendRequest('thread/resume', {
|
|
@@ -158,9 +161,11 @@ export class CodexAppServerAdapter {
|
|
|
158
161
|
this.currentTurnReject = reject;
|
|
159
162
|
});
|
|
160
163
|
turnPromise.catch(() => { });
|
|
164
|
+
const turnInput = await this.buildTurnInput(prompt, imagePaths);
|
|
165
|
+
this.trace('turn/start sent');
|
|
161
166
|
const turnStarted = await this.sendRequest('turn/start', {
|
|
162
167
|
threadId: this.threadId,
|
|
163
|
-
input:
|
|
168
|
+
input: turnInput,
|
|
164
169
|
...(this.model ? { model: this.model } : {}),
|
|
165
170
|
...this.sandboxPolicyPayload(_extraAddDirs),
|
|
166
171
|
collaborationMode: {
|
|
@@ -172,6 +177,7 @@ export class CodexAppServerAdapter {
|
|
|
172
177
|
},
|
|
173
178
|
},
|
|
174
179
|
});
|
|
180
|
+
this.trace('turn/start ack');
|
|
175
181
|
const turn = turnStarted.turn;
|
|
176
182
|
if (this.currentTurnResolve) {
|
|
177
183
|
this.currentTurnId = readString(turn, 'id') ?? null;
|
|
@@ -322,6 +328,7 @@ export class CodexAppServerAdapter {
|
|
|
322
328
|
const message = parseJson(line);
|
|
323
329
|
if (!message)
|
|
324
330
|
return;
|
|
331
|
+
this.traceLine(line, message);
|
|
325
332
|
if ('id' in message && ('result' in message || 'error' in message) && !('method' in message)) {
|
|
326
333
|
const id = Number(message.id);
|
|
327
334
|
const pending = this.pending.get(id);
|
|
@@ -393,9 +400,9 @@ export class CodexAppServerAdapter {
|
|
|
393
400
|
const delta = readRawString(params, 'delta') ?? '';
|
|
394
401
|
const next = `${this.messageTextByItem.get(itemId) ?? ''}${delta}`;
|
|
395
402
|
this.messageTextByItem.set(itemId, next);
|
|
396
|
-
this.currentFinalMessage =
|
|
403
|
+
this.currentFinalMessage = this.joinedAgentMessageText() ?? this.currentFinalMessage;
|
|
397
404
|
if (next.trim())
|
|
398
|
-
this.currentOnEvent?.({ type: 'message', text: next, itemId });
|
|
405
|
+
this.currentOnEvent?.({ type: 'message', text: next, delta, itemId });
|
|
399
406
|
return;
|
|
400
407
|
}
|
|
401
408
|
if (method === 'turn/plan/updated') {
|
|
@@ -432,7 +439,8 @@ export class CodexAppServerAdapter {
|
|
|
432
439
|
const itemId = readString(item, 'id');
|
|
433
440
|
const text = readString(item, 'text');
|
|
434
441
|
if (text) {
|
|
435
|
-
this.
|
|
442
|
+
this.messageTextByItem.set(itemId ?? 'agent-message', text);
|
|
443
|
+
this.currentFinalMessage = this.joinedAgentMessageText() ?? text;
|
|
436
444
|
this.currentOnEvent?.({
|
|
437
445
|
type: 'message',
|
|
438
446
|
text,
|
|
@@ -466,6 +474,7 @@ export class CodexAppServerAdapter {
|
|
|
466
474
|
return;
|
|
467
475
|
}
|
|
468
476
|
if (method === 'turn/completed') {
|
|
477
|
+
this.trace('turn/completed notification');
|
|
469
478
|
const turn = params.turn;
|
|
470
479
|
const status = turn?.status;
|
|
471
480
|
if (isRecord(status) && status.type === 'failed') {
|
|
@@ -486,6 +495,18 @@ export class CodexAppServerAdapter {
|
|
|
486
495
|
this.currentErrorText = stringifyPreview(params);
|
|
487
496
|
}
|
|
488
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* The turn's final message is the JOIN of every agentMessage item, in
|
|
500
|
+
* insertion order — not just the last item. The streamed turn trail folds
|
|
501
|
+
* all items (one text segment per item), so the final text must match the
|
|
502
|
+
* streamed/folded text or the app renders the turn twice.
|
|
503
|
+
*/
|
|
504
|
+
joinedAgentMessageText() {
|
|
505
|
+
const joined = [...this.messageTextByItem.values()]
|
|
506
|
+
.filter((text) => text.trim())
|
|
507
|
+
.join('\n\n');
|
|
508
|
+
return joined.trim() ? joined : null;
|
|
509
|
+
}
|
|
489
510
|
isCurrentThreadNotification(params) {
|
|
490
511
|
const threadId = readNotificationThreadId(params);
|
|
491
512
|
if (threadId && this.threadId && threadId !== this.threadId)
|
|
@@ -517,6 +538,7 @@ export class CodexAppServerAdapter {
|
|
|
517
538
|
this.currentErrorText = null;
|
|
518
539
|
this.messageTextByItem.clear();
|
|
519
540
|
this.planText = '';
|
|
541
|
+
this.traceTurnEpochMs = null;
|
|
520
542
|
}
|
|
521
543
|
sendRequest(method, params) {
|
|
522
544
|
const id = this.requestSeq++;
|
|
@@ -536,6 +558,33 @@ export class CodexAppServerAdapter {
|
|
|
536
558
|
throw new Error('Codex app-server is not running');
|
|
537
559
|
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
538
560
|
}
|
|
561
|
+
trace(message) {
|
|
562
|
+
if (!isCodexTraceEnabled())
|
|
563
|
+
return;
|
|
564
|
+
const elapsedMs = this.traceTurnEpochMs === null ? 0 : Date.now() - this.traceTurnEpochMs;
|
|
565
|
+
console.error(`[canon-codex-trace] +${elapsedMs}ms ${message}`);
|
|
566
|
+
}
|
|
567
|
+
traceLine(line, message) {
|
|
568
|
+
if (!isCodexTraceEnabled())
|
|
569
|
+
return;
|
|
570
|
+
const method = typeof message.method === 'string' ? message.method : null;
|
|
571
|
+
if (!method) {
|
|
572
|
+
const id = 'id' in message ? ` id=${String(message.id)}` : '';
|
|
573
|
+
this.trace(`line bytes=${line.length} response${id}`);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (method === 'item/agentMessage/delta') {
|
|
577
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
578
|
+
const itemId = readString(params, 'itemId') ?? 'agent-message';
|
|
579
|
+
const delta = readRawString(params, 'delta') ?? '';
|
|
580
|
+
this.trace(`line bytes=${line.length} method=${method} itemId=${itemId} deltaLen=${delta.length}`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
this.trace(`line bytes=${line.length} method=${method}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function isCodexTraceEnabled() {
|
|
587
|
+
return process.env.CANON_CODEX_TRACE_EVENTS === '1';
|
|
539
588
|
}
|
|
540
589
|
function parseJson(line) {
|
|
541
590
|
try {
|
package/dist/host.js
CHANGED
|
@@ -26,7 +26,7 @@ import { detectCodexCliVersion } from './codex-cli-version.js';
|
|
|
26
26
|
import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
|
|
27
27
|
import { attachCodexControlNotifications } from './control-channel.js';
|
|
28
28
|
import { runCli } from './cli-entry.js';
|
|
29
|
-
import { beginCommandBlock, claimCommandBlock, createCommandBlockTracker,
|
|
29
|
+
import { beginCommandBlock, applyCodexMessageToStreamingOutput, claimCommandBlock, createCommandBlockTracker, hasSpeechSegmentText, } from './turn-activity.js';
|
|
30
30
|
const HELP = `canon-codex — run a local Codex agent host for Canon
|
|
31
31
|
|
|
32
32
|
USAGE
|
|
@@ -108,6 +108,35 @@ const CODEX_RUNTIME_CAPABILITIES = {
|
|
|
108
108
|
supportsQueue: true,
|
|
109
109
|
supportsNonFinalPermanentMessages: false,
|
|
110
110
|
};
|
|
111
|
+
function isCodexTraceEnabled() {
|
|
112
|
+
return process.env.CANON_CODEX_TRACE_EVENTS === '1';
|
|
113
|
+
}
|
|
114
|
+
function createCodexTracePort(port) {
|
|
115
|
+
if (!isCodexTraceEnabled())
|
|
116
|
+
return port;
|
|
117
|
+
return new Proxy(port, {
|
|
118
|
+
get(target, property, receiver) {
|
|
119
|
+
if (property !== 'publishStreaming') {
|
|
120
|
+
return Reflect.get(target, property, receiver);
|
|
121
|
+
}
|
|
122
|
+
return async (input) => {
|
|
123
|
+
const startedAt = Date.now();
|
|
124
|
+
const textLength = typeof input.text === 'string' ? input.text.length : 0;
|
|
125
|
+
console.error(`[canon-codex-trace] publishStreaming sent conversation=${input.conversationId} turn=${input.turnId ?? input.messageId ?? 'unknown'} textLen=${textLength}`);
|
|
126
|
+
try {
|
|
127
|
+
const result = await target.publishStreaming(input);
|
|
128
|
+
console.error(`[canon-codex-trace] publishStreaming ack conversation=${input.conversationId} ackMs=${Date.now() - startedAt}`);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
console.error(`[canon-codex-trace] publishStreaming error conversation=${input.conversationId} ackMs=${Date.now() - startedAt} error=${message}`);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
111
140
|
// This host process resolves and locks exactly one agent profile. The lock
|
|
112
141
|
// handle returned by resolveCanonAgent is held here so the top-level runCli
|
|
113
142
|
// error handler (outside main's scope) can release it on a failed start —
|
|
@@ -554,7 +583,7 @@ export async function main() {
|
|
|
554
583
|
});
|
|
555
584
|
// The BridgeClient's zod-derived result shapes mirror the core wire types
|
|
556
585
|
// structurally; the port pins the strong types the host machinery uses.
|
|
557
|
-
const port = bridge.client;
|
|
586
|
+
const port = createCodexTracePort(bridge.client);
|
|
558
587
|
bridge.client.onProtocolError((error) => {
|
|
559
588
|
console.error(`[canon-codex] Bridge protocol error: ${error.message}`);
|
|
560
589
|
});
|
|
@@ -976,7 +1005,12 @@ export async function main() {
|
|
|
976
1005
|
session.turnState = 'streaming';
|
|
977
1006
|
writers.writeTurn();
|
|
978
1007
|
writers.stopVisibleWorkSignal();
|
|
979
|
-
writers.streamingOutput
|
|
1008
|
+
applyCodexMessageToStreamingOutput(writers.streamingOutput, {
|
|
1009
|
+
turnId: session.currentTurnId,
|
|
1010
|
+
itemId: event.itemId,
|
|
1011
|
+
text: event.text,
|
|
1012
|
+
delta: event.delta,
|
|
1013
|
+
});
|
|
980
1014
|
return;
|
|
981
1015
|
}
|
|
982
1016
|
if (event.type === 'plan.updated') {
|
|
@@ -990,7 +1024,16 @@ export async function main() {
|
|
|
990
1024
|
title: 'Plan',
|
|
991
1025
|
text: event.text,
|
|
992
1026
|
});
|
|
993
|
-
writers.streamingOutput.
|
|
1027
|
+
if (hasSpeechSegmentText(writers.streamingOutput.getBlocks())) {
|
|
1028
|
+
// Speech is already flowing: publish the staged plan block
|
|
1029
|
+
// only. Replacing the whole snapshot with plan text would be
|
|
1030
|
+
// undone by the next agentMessage snapshot and make the
|
|
1031
|
+
// smoothed bubble snap back and forth.
|
|
1032
|
+
writers.streamingOutput.flush().catch(() => { });
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
writers.streamingOutput.replaceSnapshot(event.text, 'streaming').catch(() => { });
|
|
1036
|
+
}
|
|
994
1037
|
return;
|
|
995
1038
|
}
|
|
996
1039
|
if (event.type === 'waiting') {
|
|
@@ -1001,9 +1044,6 @@ export async function main() {
|
|
|
1001
1044
|
return;
|
|
1002
1045
|
}
|
|
1003
1046
|
if (event.type === 'command.started') {
|
|
1004
|
-
session.turnState = 'tool';
|
|
1005
|
-
writers.writeTurn();
|
|
1006
|
-
writers.startVisibleWorkSignal();
|
|
1007
1047
|
const blockId = beginCommandBlock(session.turnCommandBlocks, {
|
|
1008
1048
|
turnId: session.currentTurnId,
|
|
1009
1049
|
command: event.command,
|
|
@@ -1016,6 +1056,16 @@ export async function main() {
|
|
|
1016
1056
|
title: summarizeCommand(event.command),
|
|
1017
1057
|
summary: 'Command running',
|
|
1018
1058
|
});
|
|
1059
|
+
if (hasSpeechSegmentText(writers.streamingOutput.getBlocks())) {
|
|
1060
|
+
// Speech is already flowing: the staged tool block narrates
|
|
1061
|
+
// the command in the margin. Publish it without flapping the
|
|
1062
|
+
// bubble status away from 'streaming'.
|
|
1063
|
+
writers.streamingOutput.flush().catch(() => { });
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
session.turnState = 'tool';
|
|
1067
|
+
writers.writeTurn();
|
|
1068
|
+
writers.startVisibleWorkSignal();
|
|
1019
1069
|
writers.streamingOutput.setStatus('tool').catch(() => { });
|
|
1020
1070
|
return;
|
|
1021
1071
|
}
|
|
@@ -1034,12 +1084,18 @@ export async function main() {
|
|
|
1034
1084
|
summary: 'Command completed',
|
|
1035
1085
|
});
|
|
1036
1086
|
}
|
|
1037
|
-
if (session.turnState === 'tool'
|
|
1087
|
+
if (session.turnState === 'tool'
|
|
1088
|
+
&& !hasSpeechSegmentText(writers.streamingOutput.getBlocks())) {
|
|
1038
1089
|
session.turnState = 'thinking';
|
|
1039
1090
|
writers.writeTurn();
|
|
1040
1091
|
writers.startVisibleWorkSignal();
|
|
1041
1092
|
writers.streamingOutput.setStatus('thinking').catch(() => { });
|
|
1042
1093
|
}
|
|
1094
|
+
else {
|
|
1095
|
+
// Speech is flowing (or the turn never left streaming):
|
|
1096
|
+
// publish the staged completion without a status flap.
|
|
1097
|
+
writers.streamingOutput.flush().catch(() => { });
|
|
1098
|
+
}
|
|
1043
1099
|
return;
|
|
1044
1100
|
}
|
|
1045
1101
|
if (event.type === 'turn.completed') {
|
package/dist/turn-activity.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import type { TurnOutputBlock } from '@canonmsg/core/contract';
|
|
1
2
|
interface RunningCommandBlock {
|
|
2
3
|
command: string;
|
|
3
4
|
blockId: string;
|
|
4
5
|
itemId?: string;
|
|
5
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* True once the streaming output already carries assistant speech (folded
|
|
9
|
+
* text segments). While speech is flowing the host must not flap the bubble
|
|
10
|
+
* status to tool/thinking or clobber the live text with plan snapshots — the
|
|
11
|
+
* staged margin blocks already narrate that activity, and every status/text
|
|
12
|
+
* swap makes the smoothed bubble snap.
|
|
13
|
+
*/
|
|
14
|
+
export declare function hasSpeechSegmentText(blocks: ReadonlyArray<Pick<TurnOutputBlock, 'kind' | 'text'>>): boolean;
|
|
6
15
|
export interface CommandBlockTracker {
|
|
7
16
|
sequence: number;
|
|
8
17
|
running: RunningCommandBlock[];
|
|
@@ -15,6 +24,16 @@ export declare function createCommandBlockTracker(): CommandBlockTracker;
|
|
|
15
24
|
* only owns the codex id scheme.
|
|
16
25
|
*/
|
|
17
26
|
export declare function textSegmentBlockId(turnId: string | null | undefined, itemId?: string): string;
|
|
27
|
+
export interface CodexTextSegmentOutput {
|
|
28
|
+
appendTextSegmentDelta(id: string, delta: string): void;
|
|
29
|
+
replaceTextSegmentSnapshot(id: string, text: string): void;
|
|
30
|
+
}
|
|
31
|
+
export declare function applyCodexMessageToStreamingOutput(output: CodexTextSegmentOutput, input: {
|
|
32
|
+
turnId: string | null | undefined;
|
|
33
|
+
itemId?: string;
|
|
34
|
+
text: string;
|
|
35
|
+
delta?: string;
|
|
36
|
+
}): void;
|
|
18
37
|
export declare function beginCommandBlock(tracker: CommandBlockTracker, input: {
|
|
19
38
|
turnId: string | null | undefined;
|
|
20
39
|
command: string;
|
package/dist/turn-activity.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True once the streaming output already carries assistant speech (folded
|
|
3
|
+
* text segments). While speech is flowing the host must not flap the bubble
|
|
4
|
+
* status to tool/thinking or clobber the live text with plan snapshots — the
|
|
5
|
+
* staged margin blocks already narrate that activity, and every status/text
|
|
6
|
+
* swap makes the smoothed bubble snap.
|
|
7
|
+
*/
|
|
8
|
+
export function hasSpeechSegmentText(blocks) {
|
|
9
|
+
return blocks.some((block) => block.kind === 'text' && Boolean(block.text?.trim()));
|
|
10
|
+
}
|
|
1
11
|
export function createCommandBlockTracker() {
|
|
2
12
|
return {
|
|
3
13
|
sequence: 0,
|
|
@@ -17,6 +27,14 @@ function normalizeOptionalString(value) {
|
|
|
17
27
|
export function textSegmentBlockId(turnId, itemId) {
|
|
18
28
|
return `message:${turnId ?? 'turn'}:${normalizeOptionalString(itemId) ?? 'latest'}`;
|
|
19
29
|
}
|
|
30
|
+
export function applyCodexMessageToStreamingOutput(output, input) {
|
|
31
|
+
const segmentId = textSegmentBlockId(input.turnId, input.itemId);
|
|
32
|
+
if (input.delta !== undefined) {
|
|
33
|
+
output.appendTextSegmentDelta(segmentId, input.delta);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
output.replaceTextSegmentSnapshot(segmentId, input.text);
|
|
37
|
+
}
|
|
20
38
|
function nextCommandBlockId(tracker, turnId, itemId) {
|
|
21
39
|
const stableTurnId = turnId ?? 'turn';
|
|
22
40
|
if (itemId)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"prepack": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@canonmsg/agent-host": "^0.
|
|
33
|
-
"@canonmsg/agent-sdk": "^3.4.
|
|
34
|
-
"@canonmsg/backend-contracts": "^1.8.
|
|
35
|
-
"@canonmsg/bridge": "^0.2.
|
|
36
|
-
"@canonmsg/core": "^3.
|
|
37
|
-
"@canonmsg/framework": "^0.2.
|
|
32
|
+
"@canonmsg/agent-host": "^0.4.0",
|
|
33
|
+
"@canonmsg/agent-sdk": "^3.4.4",
|
|
34
|
+
"@canonmsg/backend-contracts": "^1.8.1",
|
|
35
|
+
"@canonmsg/bridge": "^0.2.1",
|
|
36
|
+
"@canonmsg/core": "^3.2.0",
|
|
37
|
+
"@canonmsg/framework": "^0.2.1"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18.0.0"
|