@khorsheed/dsh-local-agent-codex 0.2.0-rc.3 → 0.2.1
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/CHANGELOG.md +23 -1
- package/README.en.md +23 -136
- package/README.i18n.yaml +2 -2
- package/README.md +23 -136
- package/lib/client.js +159 -74
- package/lib/index.js +320 -150
- package/lib/types/client/SettingsCard.js +11 -3
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/index.js +2 -2
- package/lib/types/codex-cli-provider.d.ts +33 -4
- package/lib/types/codex-cli-provider.js +144 -43
- package/lib/types/index.js +4 -2
- package/lib/types/live-driver.d.ts +0 -5
- package/lib/types/live-driver.js +231 -106
- package/lib/types/model-broker.js +42 -1
- package/lib/types/records.d.ts +6 -4
- package/lib/types/records.js +13 -6
- package/package.json +2 -2
|
@@ -7,7 +7,7 @@ import { IconChevronDownOutlineMedium } from './icons.js';
|
|
|
7
7
|
// the block carries no runtime identity to share.
|
|
8
8
|
import { ProviderAuthBlock, } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
|
|
9
9
|
import { AuthStatusDot } from '@khorsheed/dsh-local-agent/src/client/AuthStatusDot.tsx';
|
|
10
|
-
import { publishAuthStatus, useHarnessAuthStatus } from '@khorsheed/dsh-local-agent/src/client/auth-status.ts';
|
|
10
|
+
import { publishAuthStatus, useHarnessAuthStatus, usePublishedAuthStatus } from '@khorsheed/dsh-local-agent/src/client/auth-status.ts';
|
|
11
11
|
import { ModelListSection } from '@khorsheed/dsh-local-agent/src/client/ModelListSection.tsx';
|
|
12
12
|
import css from './SettingsCard.module.css';
|
|
13
13
|
/**
|
|
@@ -43,6 +43,10 @@ function useCardState(active, useSettings, scope, endpointAuth) {
|
|
|
43
43
|
const endpointUnavailable = endpointAuth === undefined || endpointState === 'unavailable';
|
|
44
44
|
const endpointLoading = !endpointUnavailable && endpointState === null;
|
|
45
45
|
const apiKeyConfigured = endpointState !== null && endpointState !== 'unavailable' && endpointState.apiKeySet;
|
|
46
|
+
// A login or logout elsewhere in the card (the auth block) changes what
|
|
47
|
+
// the store holds — logout deletes the saved key: re-read, or the form
|
|
48
|
+
// keeps saying 已配置 for a key that is gone.
|
|
49
|
+
const authPublished = usePublishedAuthStatus('codex');
|
|
46
50
|
useEffect(() => {
|
|
47
51
|
if (!active || endpointAuth === undefined)
|
|
48
52
|
return;
|
|
@@ -51,7 +55,7 @@ function useCardState(active, useSettings, scope, endpointAuth) {
|
|
|
51
55
|
setEndpointState(state ?? 'unavailable'); }, () => { if (!stale)
|
|
52
56
|
setEndpointState('unavailable'); });
|
|
53
57
|
return () => { stale = true; };
|
|
54
|
-
}, [active, endpointAuth, endpointSaved]);
|
|
58
|
+
}, [active, endpointAuth, endpointSaved, authPublished]);
|
|
55
59
|
// The credential save: one `configureEndpoint` call carrying only the
|
|
56
60
|
// dirty fields (a blank draft clears its field); the answer is the
|
|
57
61
|
// post-write resolved state, so the chips and the auth probe flip without
|
|
@@ -82,7 +86,11 @@ function useCardState(active, useSettings, scope, endpointAuth) {
|
|
|
82
86
|
const write = (field, value) => {
|
|
83
87
|
setSaved(false);
|
|
84
88
|
setError(false);
|
|
85
|
-
|
|
89
|
+
// The host answers a refused write with `false` (ConfigForm), not a rejection.
|
|
90
|
+
void scope.set(field, value).then((accepted) => { if (accepted === false)
|
|
91
|
+
setError(true);
|
|
92
|
+
else
|
|
93
|
+
setSaved(true); }, () => { setError(true); });
|
|
86
94
|
};
|
|
87
95
|
return {
|
|
88
96
|
snapshot, saved, error, ready, live, write,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
|
|
3
|
-
* (v0.1
|
|
3
|
+
* (v0.2.1-rc.1, harness 1600d8a1e1) — do not edit by hand. The 0.1.5 and rc.1 host lines
|
|
4
4
|
* share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
|
|
5
5
|
* no host runtime identity, identical on every line. Re-run the generator
|
|
6
6
|
* after adding an icon import or bumping the harness checkout.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bindCardSettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Local-agent-codex plugin, browser half: the codex settings card — a section
|
|
4
4
|
* of the family core's「本机 Agent 委派」family card on the Plugins page
|
|
@@ -46,7 +46,7 @@ export const inject = ['slots', 'remote', 'remote.commands', 'locale'];
|
|
|
46
46
|
export function apply(ctx) {
|
|
47
47
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'local-agent-codex: dictionaries');
|
|
48
48
|
const t = ctx.locale.bind(NS);
|
|
49
|
-
const scope =
|
|
49
|
+
const scope = bindCardSettingsScope(ctx, 'codex', 'local-agent-codex');
|
|
50
50
|
// The auth block's copy lives in the family core's dictionary; binding is
|
|
51
51
|
// stable per namespace and late dictionary registration still resolves.
|
|
52
52
|
const authT = ctx.locale.bind('local-agent');
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* it too (a deliberate user re-enable conflicts loud, by design).
|
|
14
14
|
* @module @khorsheed/dsh-local-agent-codex/codex-cli-provider
|
|
15
15
|
*/
|
|
16
|
-
import type { ContentBlock, TokenUsage } from '@deepseek-ai/dsh-llm';
|
|
16
|
+
import type { ContentBlock, ReasoningEffortId, TokenUsage } from '@deepseek-ai/dsh-llm';
|
|
17
17
|
import { type ResolvedSubagentStartRequest, type SubagentCapabilities, type SubagentProvider, type SubagentRun, type SubagentStartRequest, type SubagentStopReason } from '@deepseek-ai/dsh-subagent';
|
|
18
18
|
import type { Context } from '@deepseek-ai/cordis';
|
|
19
19
|
import type { Session } from '@deepseek-ai/dsh-session';
|
|
@@ -21,6 +21,14 @@ import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-sub
|
|
|
21
21
|
import type { DelegationExecTarget, LocalAgentToolCalls } from '@khorsheed/dsh-local-agent/types';
|
|
22
22
|
import type { CodexLiveDriver } from './live-driver.ts';
|
|
23
23
|
import { type CodexEndpointAuth } from './endpoint-auth.ts';
|
|
24
|
+
/**
|
|
25
|
+
* The synthesized result for a call whose result the stream never recorded
|
|
26
|
+
* (a killed run, or a result-less item type like web_search_call). The
|
|
27
|
+
* released-log contract resolves every advertised call before its step
|
|
28
|
+
* closes, so an interrupted call folds with this error result. Same text as
|
|
29
|
+
* the kimi provider's placeholder, for family consistency.
|
|
30
|
+
*/
|
|
31
|
+
export declare const CODEX_INTERRUPTED_TOOL_RESULT = "\uFF08\u5DE5\u5177\u8C03\u7528\u88AB\u4E2D\u65AD\uFF0C\u672A\u80FD\u8BB0\u5F55\u8FD4\u56DE\u7ED3\u679C\uFF09";
|
|
24
32
|
/** Default POSIX grace between subprocess termination tiers. */
|
|
25
33
|
export declare const DEFAULT_DISPOSE_GRACE_MS = 3000;
|
|
26
34
|
/**
|
|
@@ -308,7 +316,8 @@ export declare class CodexStreamParser implements CodexStreamFoldState {
|
|
|
308
316
|
* not put one on the wire; the settle path then reads the rollout's
|
|
309
317
|
* turn_context). Item types covered: `reasoning` (thinking), `agent_message`
|
|
310
318
|
* (reply text), `command_execution` (shell command with aggregated output),
|
|
311
|
-
* `web_search_call`,
|
|
319
|
+
* `web_search_call`, `custom_tool_call`/`custom_tool_call_output` (deferred
|
|
320
|
+
* and dynamic tools), and `function_call_output`. The last `agent_message`
|
|
312
321
|
* wins as the run output (the final answer), and `turn.completed` carries the
|
|
313
322
|
* turn's usage. Malformed lines are skipped.
|
|
314
323
|
* @param stream - the collected stdout NDJSON text.
|
|
@@ -336,6 +345,17 @@ export declare function parseCodexJsonStream(stream: string): {
|
|
|
336
345
|
* @returns the published run after the child starts.
|
|
337
346
|
*/
|
|
338
347
|
export declare function startCodexCliRun(request: SubagentStartRequest, spec: CodexCliRunSpec): Promise<SubagentRun>;
|
|
348
|
+
/**
|
|
349
|
+
* Write the round's request/header when the config differs from the child
|
|
350
|
+
* log's last one (the host logs changed snapshots, not per-call duplicates):
|
|
351
|
+
* the first write is `initial`, a differing one `change`, an identical one
|
|
352
|
+
* skipped. Context Lens reads the child log's config from these headers.
|
|
353
|
+
*/
|
|
354
|
+
export declare function appendCodexRequestHeader(childSession: Session, config: {
|
|
355
|
+
provider: string;
|
|
356
|
+
model: string;
|
|
357
|
+
reasoningEffort?: ReasoningEffortId;
|
|
358
|
+
}): void;
|
|
339
359
|
/** One assistant-role message event, attributed to the codex route. */
|
|
340
360
|
export declare function codexAssistantEvent(blocks: readonly ContentBlock[]): import("@deepseek-ai/dsh-llm").AssistantMessage;
|
|
341
361
|
/**
|
|
@@ -344,8 +364,17 @@ export declare function codexAssistantEvent(blocks: readonly ContentBlock[]): im
|
|
|
344
364
|
* assembler requires: without a boundary the step never enters the location
|
|
345
365
|
* index, the line's assistant message resolves to a turn-level location, and
|
|
346
366
|
* the real-time view renders nothing (only a full rebuild, which mints step
|
|
347
|
-
* drafts from the explicit coordinates, recovers it).
|
|
348
|
-
*
|
|
367
|
+
* drafts from the explicit coordinates, recovers it).
|
|
368
|
+
*
|
|
369
|
+
* A tool line folds the NATIVE lifecycle inside its one step: an assistant
|
|
370
|
+
* message advertising the tool-call block, the `tool/call`, then its
|
|
371
|
+
* `tool/result` — the released-log reader pairs call and result against the
|
|
372
|
+
* advertisement and resolves every advertised call before the step closes.
|
|
373
|
+
* Every fold sees a COMPLETED item (the exec live mirror holds the volatile
|
|
374
|
+
* last line; the live driver folds at item/completed), so a result the
|
|
375
|
+
* stream has not recorded by fold time will never arrive — the call folds
|
|
376
|
+
* with the synthesized interrupted result instead of leaving the step
|
|
377
|
+
* unresolvable.
|
|
349
378
|
* @returns whether the line folded (false when the options skipped it).
|
|
350
379
|
*/
|
|
351
380
|
export declare function appendCodexTranscriptLine(childSession: Session, turn: number, step: number, line: CodexTranscriptLine, usage: TokenUsage | undefined): boolean;
|
|
@@ -23,6 +23,14 @@ import { LiveChannelUnavailableError } from './live-driver.js';
|
|
|
23
23
|
import { endpointAuthEnv, endpointConfigOverrides, hasEndpointCredential } from './endpoint-auth.js';
|
|
24
24
|
import { readCodexBaseUrl } from './provision.js';
|
|
25
25
|
import { codexRolloutRoundFacts, usageFromCodex } from './records.js';
|
|
26
|
+
/**
|
|
27
|
+
* The synthesized result for a call whose result the stream never recorded
|
|
28
|
+
* (a killed run, or a result-less item type like web_search_call). The
|
|
29
|
+
* released-log contract resolves every advertised call before its step
|
|
30
|
+
* closes, so an interrupted call folds with this error result. Same text as
|
|
31
|
+
* the kimi provider's placeholder, for family consistency.
|
|
32
|
+
*/
|
|
33
|
+
export const CODEX_INTERRUPTED_TOOL_RESULT = '(工具调用被中断,未能记录返回结果)';
|
|
26
34
|
/** Quote one TOML basic string for the `-c` config override. */
|
|
27
35
|
function tomlString(value) {
|
|
28
36
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
@@ -50,7 +58,10 @@ export function registerCodexMemberRun(ctx, childSessionId, parentSessionId) {
|
|
|
50
58
|
+ `command=${tomlString(bridge.command)},`
|
|
51
59
|
+ `args=[${bridge.args.map(tomlString).join(',')}],`
|
|
52
60
|
+ `env={${MEMBER_BRIDGE_SOCKET_ENV}=${tomlString(registry.memberBridgeSocketPath())},${MEMBER_BRIDGE_TOKEN_ENV}=${tomlString(token)}},`
|
|
53
|
-
+ `default_tools_approval_mode=${tomlString('approve')}
|
|
61
|
+
+ `default_tools_approval_mode=${tomlString('approve')},`
|
|
62
|
+
// A bridge call may wait for the human (ask_user_question, a mode tool's
|
|
63
|
+
// approval): Codex's 60s MCP default would fail it first.
|
|
64
|
+
+ `tool_timeout_sec=1800`
|
|
54
65
|
+ `}`;
|
|
55
66
|
let released = false;
|
|
56
67
|
return {
|
|
@@ -651,6 +662,40 @@ function foldCodexStreamLine(state, raw) {
|
|
|
651
662
|
}
|
|
652
663
|
}
|
|
653
664
|
}
|
|
665
|
+
else if (item.type === 'custom_tool_call') {
|
|
666
|
+
// Custom tools (the deferred `exec` discovery tool, dynamic tools): the
|
|
667
|
+
// call rides `input`, its result the paired custom_tool_call_output.
|
|
668
|
+
// Without this branch a tool-loop round mirrored as two bare adjacent
|
|
669
|
+
// replies with the call between them invisible.
|
|
670
|
+
countToolCall(state, 'custom_tool_call');
|
|
671
|
+
state.lines.push({
|
|
672
|
+
kind: 'tool',
|
|
673
|
+
id: typeof item.id === 'string' ? item.id : `codex-tool-${state.lines.length}`,
|
|
674
|
+
name: item.name ?? 'custom',
|
|
675
|
+
...typeof item.input === 'string' && item.input !== '' ? { args: item.input } : {},
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
else if (item.type === 'custom_tool_call_output') {
|
|
679
|
+
// {call_id, output: string | content blocks} — pair by recency, the same
|
|
680
|
+
// idiom function_call_output uses (the stream orders a result right
|
|
681
|
+
// behind its call).
|
|
682
|
+
const output = typeof item.output === 'string' ? item.output
|
|
683
|
+
: Array.isArray(item.output)
|
|
684
|
+
? item.output
|
|
685
|
+
.filter((block) => typeof block === 'object' && block !== null && typeof block.text === 'string')
|
|
686
|
+
.map(block => block.text)
|
|
687
|
+
.join('\n')
|
|
688
|
+
: undefined;
|
|
689
|
+
if (output !== undefined && output.trim() !== '') {
|
|
690
|
+
const last = state.lines[state.lines.length - 1];
|
|
691
|
+
if (last !== undefined && last.kind === 'tool') {
|
|
692
|
+
state.lines[state.lines.length - 1] = {
|
|
693
|
+
...last,
|
|
694
|
+
result: last.result === undefined ? output : `${last.result}\n${output}`,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
654
699
|
}
|
|
655
700
|
/**
|
|
656
701
|
* Incremental `codex exec --json` parser: feed stdout chunks as they arrive
|
|
@@ -685,7 +730,8 @@ export class CodexStreamParser {
|
|
|
685
730
|
* not put one on the wire; the settle path then reads the rollout's
|
|
686
731
|
* turn_context). Item types covered: `reasoning` (thinking), `agent_message`
|
|
687
732
|
* (reply text), `command_execution` (shell command with aggregated output),
|
|
688
|
-
* `web_search_call`,
|
|
733
|
+
* `web_search_call`, `custom_tool_call`/`custom_tool_call_output` (deferred
|
|
734
|
+
* and dynamic tools), and `function_call_output`. The last `agent_message`
|
|
689
735
|
* wins as the run output (the final answer), and `turn.completed` carries the
|
|
690
736
|
* turn's usage. Malformed lines are skipped.
|
|
691
737
|
* @param stream - the collected stdout NDJSON text.
|
|
@@ -772,6 +818,17 @@ export function startCodexCliRun(request, spec) {
|
|
|
772
818
|
// The turn opens at the real spawn moment so the timing projection
|
|
773
819
|
// measures actual CLI runtime, not the post-hoc append time.
|
|
774
820
|
spec.childSession?.append('turn/start', { turn });
|
|
821
|
+
// The provider's own launch facts are the child log's request config —
|
|
822
|
+
// codex's stream never carries model/effort, so the argv the provider
|
|
823
|
+
// spawns with is the authoritative source. The header contract requires a
|
|
824
|
+
// model: an unconfigured round records nothing rather than inventing one.
|
|
825
|
+
if (spec.model !== undefined && spec.childSession !== undefined) {
|
|
826
|
+
appendCodexRequestHeader(spec.childSession, {
|
|
827
|
+
provider: 'codex-local',
|
|
828
|
+
model: spec.model,
|
|
829
|
+
...spec.effort === undefined ? {} : { reasoningEffort: spec.effort },
|
|
830
|
+
});
|
|
831
|
+
}
|
|
775
832
|
// Live mirror: fold the NDJSON stream as chunks arrive so the child session
|
|
776
833
|
// shows the run's progress before settle; the settle mirror below resumes
|
|
777
834
|
// from the live counters and stays a no-op when live mirroring kept up.
|
|
@@ -896,29 +953,12 @@ export function startCodexCliRun(request, spec) {
|
|
|
896
953
|
signal: request.signal,
|
|
897
954
|
onAbort,
|
|
898
955
|
}).then((settled) => {
|
|
899
|
-
// Every terminal path closes the turn so the timing window never stays
|
|
900
|
-
// open on a failed or cancelled run: success settles 'completed', a
|
|
901
|
-
// non-zero exit 'error', and a locally cancelled run 'aborted'. The
|
|
902
|
-
// turn/end timestamp is the real settle moment.
|
|
903
956
|
// A completed round proves the scoped credential is live — the endpoint
|
|
904
957
|
// answered. Reported here rather than from the mirror so it lands whether
|
|
905
|
-
// or not the round is session-backed.
|
|
958
|
+
// or not the round is session-backed. The turn boundary itself is the
|
|
959
|
+
// post-exit chain's job (it must follow the settle mirror).
|
|
906
960
|
if (settled.stopReason === 'completed')
|
|
907
961
|
spec.onAuthSuccess?.();
|
|
908
|
-
if (spec.childSession !== undefined) {
|
|
909
|
-
if (settled.stopReason === 'completed') {
|
|
910
|
-
spec.childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
911
|
-
}
|
|
912
|
-
else if (settled.stopReason === 'aborted') {
|
|
913
|
-
spec.childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
914
|
-
}
|
|
915
|
-
else {
|
|
916
|
-
spec.childSession.append('turn/end', {
|
|
917
|
-
turn,
|
|
918
|
-
reason: { kind: 'error', error: { message: `codex exec exited with code ${String(exitCode)}`, code: 'UNKNOWN' } },
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
962
|
return settled;
|
|
923
963
|
});
|
|
924
964
|
// After the child EXITS — however it ended (completed, killed by dispose, or
|
|
@@ -926,9 +966,13 @@ export function startCodexCliRun(request, spec) {
|
|
|
926
966
|
// subagent session, so a cancelled round still preserves its partial work
|
|
927
967
|
// (reasoning, commands, replies) and real token usage. The thread id is
|
|
928
968
|
// recorded even on abort so the partial thread stays resumable. Waits for
|
|
929
|
-
// the settle chain first
|
|
930
|
-
//
|
|
931
|
-
|
|
969
|
+
// the settle chain first AND for the process to actually exit (so stdout is
|
|
970
|
+
// drained before parsing). The turn closes AFTER the settle mirror, still
|
|
971
|
+
// serialized behind the live mirror's queue: every mirrored step must land
|
|
972
|
+
// before turn/end — the released-log reader rejects a step outside an open
|
|
973
|
+
// turn on every cold read. The run's result still settles at the real
|
|
974
|
+
// moment (abort latency unchanged); only the boundary event moves.
|
|
975
|
+
void result.then(() => child.done).then(async () => {
|
|
932
976
|
if (spec.onAuthFailure !== undefined) {
|
|
933
977
|
// The seam's collected buffers are complete at process exit; the
|
|
934
978
|
// streamed variables can lag `done` by a tick.
|
|
@@ -939,7 +983,30 @@ export function startCodexCliRun(request, spec) {
|
|
|
939
983
|
spec.onAuthFailure(authDetail.split('\n').find(line => line.trim() !== '') ?? 'auth failure');
|
|
940
984
|
}
|
|
941
985
|
}
|
|
942
|
-
|
|
986
|
+
await mirrorCodexAfterExit(spec, task, turn, output, liveMirror, startedAtMs);
|
|
987
|
+
// Every terminal path closes the turn so the timing window never stays
|
|
988
|
+
// open on a failed or cancelled run: success settles 'completed', a
|
|
989
|
+
// non-zero exit 'error', and a locally cancelled run 'aborted'.
|
|
990
|
+
if (spec.childSession !== undefined) {
|
|
991
|
+
const settled = await result;
|
|
992
|
+
if (settled.stopReason === 'completed') {
|
|
993
|
+
spec.childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
994
|
+
}
|
|
995
|
+
else if (settled.stopReason === 'aborted') {
|
|
996
|
+
spec.childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
spec.childSession.append('turn/end', {
|
|
1000
|
+
turn,
|
|
1001
|
+
reason: { kind: 'error', error: { message: `codex exec exited with code ${String(exitCode)}`, code: 'UNKNOWN' } },
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
if (spec.ctx !== undefined) {
|
|
1005
|
+
await persistChildSession(spec.ctx, spec.childSession).catch((error) => {
|
|
1006
|
+
spec.ctx?.logger.warn(`subagent-codex: turn/end persistence failed: ${thrown(error).message}`);
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
943
1010
|
}, () => { });
|
|
944
1011
|
return Promise.resolve(subprocessRunHandle({
|
|
945
1012
|
// Host 0.2.1: the published run id is a fresh PER-EXECUTION identity —
|
|
@@ -955,6 +1022,27 @@ export function startCodexCliRun(request, spec) {
|
|
|
955
1022
|
teardown: disposeProcess,
|
|
956
1023
|
}));
|
|
957
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Write the round's request/header when the config differs from the child
|
|
1027
|
+
* log's last one (the host logs changed snapshots, not per-call duplicates):
|
|
1028
|
+
* the first write is `initial`, a differing one `change`, an identical one
|
|
1029
|
+
* skipped. Context Lens reads the child log's config from these headers.
|
|
1030
|
+
*/
|
|
1031
|
+
export function appendCodexRequestHeader(childSession, config) {
|
|
1032
|
+
const serialized = JSON.stringify(config);
|
|
1033
|
+
let last;
|
|
1034
|
+
for (const event of childSession.snapshotEvents()) {
|
|
1035
|
+
if (event.type === 'request/header') {
|
|
1036
|
+
last = JSON.stringify(event.data.header?.config);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (last === serialized)
|
|
1040
|
+
return;
|
|
1041
|
+
childSession.append('request/header', {
|
|
1042
|
+
header: { config },
|
|
1043
|
+
reason: last === undefined ? 'initial' : 'change',
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
958
1046
|
/** One assistant-role message event, attributed to the codex route. */
|
|
959
1047
|
export function codexAssistantEvent(blocks) {
|
|
960
1048
|
return createAssistantMessage({
|
|
@@ -968,35 +1056,48 @@ export function codexAssistantEvent(blocks) {
|
|
|
968
1056
|
* assembler requires: without a boundary the step never enters the location
|
|
969
1057
|
* index, the line's assistant message resolves to a turn-level location, and
|
|
970
1058
|
* the real-time view renders nothing (only a full rebuild, which mints step
|
|
971
|
-
* drafts from the explicit coordinates, recovers it).
|
|
972
|
-
*
|
|
1059
|
+
* drafts from the explicit coordinates, recovers it).
|
|
1060
|
+
*
|
|
1061
|
+
* A tool line folds the NATIVE lifecycle inside its one step: an assistant
|
|
1062
|
+
* message advertising the tool-call block, the `tool/call`, then its
|
|
1063
|
+
* `tool/result` — the released-log reader pairs call and result against the
|
|
1064
|
+
* advertisement and resolves every advertised call before the step closes.
|
|
1065
|
+
* Every fold sees a COMPLETED item (the exec live mirror holds the volatile
|
|
1066
|
+
* last line; the live driver folds at item/completed), so a result the
|
|
1067
|
+
* stream has not recorded by fold time will never arrive — the call folds
|
|
1068
|
+
* with the synthesized interrupted result instead of leaving the step
|
|
1069
|
+
* unresolvable.
|
|
973
1070
|
* @returns whether the line folded (false when the options skipped it).
|
|
974
1071
|
*/
|
|
975
1072
|
export function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
|
|
976
1073
|
childSession.append('step/start', { turn, step });
|
|
977
1074
|
if (line.kind === 'tool') {
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
|
|
981
|
-
|
|
1075
|
+
// A tool-only call's usage rides the advertising assistant/message (the
|
|
1076
|
+
// tool/call and tool/result events themselves have no usage slot).
|
|
1077
|
+
const args = line.args ?? '';
|
|
1078
|
+
childSession.append('assistant/message', {
|
|
1079
|
+
turn,
|
|
1080
|
+
step,
|
|
1081
|
+
message: codexAssistantEvent([{ type: 'tool-call', id: line.id, name: line.name, arguments: args }]),
|
|
1082
|
+
stream: [],
|
|
1083
|
+
...usage === undefined ? {} : { usage },
|
|
1084
|
+
}, { surfaceOp: 'append' });
|
|
982
1085
|
const call = childSession.append('tool/call', {
|
|
983
1086
|
turn,
|
|
984
1087
|
step,
|
|
985
1088
|
callId: line.id,
|
|
986
1089
|
name: line.name,
|
|
987
|
-
arguments:
|
|
1090
|
+
arguments: args,
|
|
988
1091
|
});
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}, { surfaceOp: 'append', sourceEventSeqs: [call.seq] });
|
|
999
|
-
}
|
|
1092
|
+
childSession.append('tool/result', {
|
|
1093
|
+
turn,
|
|
1094
|
+
step,
|
|
1095
|
+
message: createToolResultMessage({
|
|
1096
|
+
callId: line.id,
|
|
1097
|
+
content: [{ type: 'text', text: line.result ?? CODEX_INTERRUPTED_TOOL_RESULT }],
|
|
1098
|
+
isError: line.result === undefined,
|
|
1099
|
+
}),
|
|
1100
|
+
}, { surfaceOp: 'append', sourceEventSeqs: [call.seq] });
|
|
1000
1101
|
childSession.append('step/end', { turn, step });
|
|
1001
1102
|
return true;
|
|
1002
1103
|
}
|
package/lib/types/index.js
CHANGED
|
@@ -49,7 +49,7 @@ export const inject = ['localAgent', 'subagents', 'subprocess', 'settings'];
|
|
|
49
49
|
* behavior byte-identical.
|
|
50
50
|
*/
|
|
51
51
|
const SETTINGS_FIELDS = {
|
|
52
|
-
live: z.boolean().default(
|
|
52
|
+
live: z.boolean().default(true),
|
|
53
53
|
liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
|
|
54
54
|
model: z.string(),
|
|
55
55
|
recentModels: z.array(z.string()).default([]),
|
|
@@ -117,7 +117,7 @@ export function apply(ctx, config) {
|
|
|
117
117
|
const model = vol(config.model);
|
|
118
118
|
const recentModels = vol(config.recentModels);
|
|
119
119
|
return {
|
|
120
|
-
live: vol(config.live) ??
|
|
120
|
+
live: vol(config.live) ?? true,
|
|
121
121
|
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
|
|
122
122
|
...model === undefined ? {} : { model },
|
|
123
123
|
...recentModels === undefined ? {} : { recentModels },
|
|
@@ -131,6 +131,8 @@ export function apply(ctx, config) {
|
|
|
131
131
|
}
|
|
132
132
|
const face = settingsFace(ctx, {
|
|
133
133
|
namespace: CODEX_SETTINGS_NAMESPACE,
|
|
134
|
+
// Card writes live in the harness's own store (card-settings.ts), never the profile patch.
|
|
135
|
+
harness: 'codex',
|
|
134
136
|
legacySchema: CODEX_SETTINGS_SCHEMA,
|
|
135
137
|
legacyBase,
|
|
136
138
|
read: readSettings,
|
|
@@ -42,11 +42,6 @@ export declare const DEFAULT_LIVE_CHANNEL_RETRY_MS: number;
|
|
|
42
42
|
export declare const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = 50;
|
|
43
43
|
/** Default minimum text growth between one streaming item's snapshot messages. */
|
|
44
44
|
export declare const DEFAULT_SNAPSHOT_MIN_CHARS = 0;
|
|
45
|
-
/**
|
|
46
|
-
* The app-server channel could not come up (spawn failure or handshake
|
|
47
|
-
* timeout/mismatch). The provider catches exactly this and falls back to the
|
|
48
|
-
* exec one-shot; any other error is a real round failure.
|
|
49
|
-
*/
|
|
50
45
|
export declare class LiveChannelUnavailableError extends Error {
|
|
51
46
|
constructor(message: string);
|
|
52
47
|
}
|