@agentex/agent 0.0.26 → 0.0.28
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 +118 -0
- package/README.md +58 -7
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +99 -2
- package/dist/derived.js.map +1 -1
- package/dist/goals/controller.d.ts.map +1 -1
- package/dist/goals/controller.js +21 -0
- package/dist/goals/controller.js.map +1 -1
- package/dist/goals/normalize.d.ts +1 -0
- package/dist/goals/normalize.d.ts.map +1 -1
- package/dist/goals/normalize.js +5 -0
- package/dist/goals/normalize.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/attach.d.ts.map +1 -1
- package/dist/providers/claude/attach.js +50 -9
- package/dist/providers/claude/attach.js.map +1 -1
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +8 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +2 -0
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +17 -10
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts.map +1 -1
- package/dist/providers/codex/attach.js +40 -3
- package/dist/providers/codex/attach.js.map +1 -1
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +8 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +6 -0
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript.d.ts +3 -1
- package/dist/providers/codex/transcript.d.ts.map +1 -1
- package/dist/providers/codex/transcript.js +3 -1
- package/dist/providers/codex/transcript.js.map +1 -1
- package/dist/providers/cursor/discovery.d.ts +6 -0
- package/dist/providers/cursor/discovery.d.ts.map +1 -0
- package/dist/providers/cursor/discovery.js +118 -0
- package/dist/providers/cursor/discovery.js.map +1 -0
- package/dist/providers/cursor/execute.d.ts.map +1 -1
- package/dist/providers/cursor/execute.js +83 -45
- package/dist/providers/cursor/execute.js.map +1 -1
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +17 -4
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/cursor/parse.js +1 -1
- package/dist/providers/cursor/parse.js.map +1 -1
- package/dist/providers/cursor/probe.d.ts +3 -0
- package/dist/providers/cursor/probe.d.ts.map +1 -0
- package/dist/providers/cursor/probe.js +89 -0
- package/dist/providers/cursor/probe.js.map +1 -0
- package/dist/providers/cursor/runtime.d.ts +4 -0
- package/dist/providers/cursor/runtime.d.ts.map +1 -0
- package/dist/providers/cursor/runtime.js +12 -0
- package/dist/providers/cursor/runtime.js.map +1 -0
- package/dist/providers/cursor/session.d.ts +3 -0
- package/dist/providers/cursor/session.d.ts.map +1 -0
- package/dist/providers/cursor/session.js +14 -0
- package/dist/providers/cursor/session.js.map +1 -0
- package/dist/providers/opencode/client.d.ts +14 -0
- package/dist/providers/opencode/client.d.ts.map +1 -0
- package/dist/providers/opencode/client.js +40 -0
- package/dist/providers/opencode/client.js.map +1 -0
- package/dist/providers/opencode/discovery.d.ts +4 -0
- package/dist/providers/opencode/discovery.d.ts.map +1 -0
- package/dist/providers/opencode/discovery.js +68 -0
- package/dist/providers/opencode/discovery.js.map +1 -0
- package/dist/providers/opencode/execute.d.ts.map +1 -1
- package/dist/providers/opencode/execute.js +219 -164
- package/dist/providers/opencode/execute.js.map +1 -1
- package/dist/providers/opencode/history.d.ts +24 -0
- package/dist/providers/opencode/history.d.ts.map +1 -0
- package/dist/providers/opencode/history.js +242 -0
- package/dist/providers/opencode/history.js.map +1 -0
- package/dist/providers/opencode/http-session.d.ts.map +1 -1
- package/dist/providers/opencode/http-session.js +333 -49
- package/dist/providers/opencode/http-session.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +26 -3
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/opencode/manager.d.ts +11 -0
- package/dist/providers/opencode/manager.d.ts.map +1 -0
- package/dist/providers/opencode/manager.js +240 -0
- package/dist/providers/opencode/manager.js.map +1 -0
- package/dist/providers/opencode/modes.d.ts +3 -0
- package/dist/providers/opencode/modes.d.ts.map +1 -0
- package/dist/providers/opencode/modes.js +30 -0
- package/dist/providers/opencode/modes.js.map +1 -0
- package/dist/providers/opencode/parse.d.ts.map +1 -1
- package/dist/providers/opencode/parse.js +2 -22
- package/dist/providers/opencode/parse.js.map +1 -1
- package/dist/providers/opencode/probe.d.ts +3 -0
- package/dist/providers/opencode/probe.d.ts.map +1 -0
- package/dist/providers/opencode/probe.js +107 -0
- package/dist/providers/opencode/probe.js.map +1 -0
- package/dist/providers/opencode/runtime.d.ts +8 -0
- package/dist/providers/opencode/runtime.d.ts.map +1 -0
- package/dist/providers/opencode/runtime.js +12 -0
- package/dist/providers/opencode/runtime.js.map +1 -0
- package/dist/providers/opencode/server.d.ts +18 -3
- package/dist/providers/opencode/server.d.ts.map +1 -1
- package/dist/providers/opencode/server.js +0 -0
- package/dist/providers/opencode/server.js.map +1 -1
- package/dist/providers/opencode/skill-config.d.ts +11 -0
- package/dist/providers/opencode/skill-config.d.ts.map +1 -0
- package/dist/providers/opencode/skill-config.js +57 -0
- package/dist/providers/opencode/skill-config.js.map +1 -0
- package/dist/sessions/exec-backed.d.ts +10 -0
- package/dist/sessions/exec-backed.d.ts.map +1 -0
- package/dist/sessions/exec-backed.js +141 -0
- package/dist/sessions/exec-backed.js.map +1 -0
- package/dist/sessions/history.d.ts +4 -0
- package/dist/sessions/history.d.ts.map +1 -0
- package/dist/sessions/history.js +59 -0
- package/dist/sessions/history.js.map +1 -0
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/types.d.ts +149 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +36 -6
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/binary.d.ts.map +1 -1
- package/dist/utils/binary.js +3 -0
- package/dist/utils/binary.js.map +1 -1
- package/dist/utils/endpoint.js +3 -3
- package/dist/utils/endpoint.js.map +1 -1
- package/package.json +4 -2
- package/src/derived.ts +126 -3
- package/src/goals/controller.ts +17 -0
- package/src/goals/normalize.ts +5 -0
- package/src/index.ts +13 -0
- package/src/providers/claude/attach.ts +48 -6
- package/src/providers/claude/index.ts +8 -0
- package/src/providers/claude/session.ts +15 -10
- package/src/providers/codex/attach.ts +48 -3
- package/src/providers/codex/index.ts +8 -0
- package/src/providers/codex/session.ts +5 -0
- package/src/providers/codex/transcript.ts +5 -1
- package/src/providers/cursor/discovery.ts +114 -0
- package/src/providers/cursor/execute.ts +67 -30
- package/src/providers/cursor/index.ts +19 -5
- package/src/providers/cursor/parse.ts +1 -1
- package/src/providers/cursor/probe.ts +90 -0
- package/src/providers/cursor/runtime.ts +11 -0
- package/src/providers/cursor/session.ts +15 -0
- package/src/providers/opencode/client.ts +44 -0
- package/src/providers/opencode/discovery.ts +69 -0
- package/src/providers/opencode/execute.ts +64 -12
- package/src/providers/opencode/history.ts +270 -0
- package/src/providers/opencode/http-session.ts +319 -50
- package/src/providers/opencode/index.ts +26 -3
- package/src/providers/opencode/manager.ts +269 -0
- package/src/providers/opencode/modes.ts +31 -0
- package/src/providers/opencode/parse.ts +2 -22
- package/src/providers/opencode/probe.ts +113 -0
- package/src/providers/opencode/runtime.ts +13 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/opencode/skill-config.ts +64 -0
- package/src/sessions/exec-backed.ts +169 -0
- package/src/sessions/history.ts +76 -0
- package/src/sessions/index.ts +3 -0
- package/src/types.ts +160 -3
- package/src/utils/auth.ts +35 -6
- package/src/utils/binary.ts +3 -0
- package/src/utils/endpoint.ts +3 -3
package/src/derived.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AgentSession,
|
|
3
|
+
AttachOptions,
|
|
4
|
+
HistoryAttachment,
|
|
2
5
|
ProviderCapabilities,
|
|
3
6
|
ProviderConfig,
|
|
4
7
|
ProviderModel,
|
|
5
8
|
ProviderModule,
|
|
9
|
+
ProviderRuntimeContext,
|
|
10
|
+
SessionAttachment,
|
|
11
|
+
SessionRecord,
|
|
6
12
|
} from "./types.js";
|
|
7
13
|
import { getProvider, registerProvider } from "./registry.js";
|
|
8
14
|
import { acpProvider } from "./providers/acp/index.js";
|
|
15
|
+
import { MalformedSessionRecordError } from "./sessions/record.js";
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Declarative configuration for a *derived* provider — a new provider id that
|
|
@@ -76,6 +83,38 @@ export function getAcpFactory(): AcpFactory | null {
|
|
|
76
83
|
return acpFactory;
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
function withProviderType(record: SessionRecord | null, providerType: string): SessionRecord | null {
|
|
87
|
+
return record ? { ...record, providerType } : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function wrapDerivedSession(session: AgentSession, providerType: string): AgentSession {
|
|
91
|
+
return new Proxy(session, {
|
|
92
|
+
get(target, property) {
|
|
93
|
+
if (property === "describe") {
|
|
94
|
+
return target.describe
|
|
95
|
+
? () => withProviderType(target.describe!(), providerType)
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
if (property === "describeHistory") {
|
|
99
|
+
return target.describeHistory
|
|
100
|
+
? () => withProviderType(target.describeHistory!(), providerType)
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
const value = Reflect.get(target, property, target) as unknown;
|
|
104
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assertDerivedRecord(record: SessionRecord, providerType: string): void {
|
|
110
|
+
if (record.providerType !== providerType) {
|
|
111
|
+
throw new MalformedSessionRecordError(
|
|
112
|
+
`${providerType} attach requires providerType ${JSON.stringify(providerType)}; got ${JSON.stringify(record.providerType)}`,
|
|
113
|
+
"providerType",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
79
118
|
/**
|
|
80
119
|
* Build a derived `ProviderModule` from a base provider plus an overlay. The
|
|
81
120
|
* returned module is NOT auto-registered — pass it to `registerProvider`, or
|
|
@@ -124,15 +163,21 @@ export function defineDerivedProvider(cfg: DerivedProviderConfig): ProviderModul
|
|
|
124
163
|
|
|
125
164
|
if (base.createSession) {
|
|
126
165
|
const baseCreate = base.createSession.bind(base);
|
|
127
|
-
derived.createSession = (ctx) =>
|
|
128
|
-
baseCreate({ ...ctx, env: overlayEnv(ctx.env), config: overlayConfig(ctx.config) })
|
|
166
|
+
derived.createSession = async (ctx) => wrapDerivedSession(
|
|
167
|
+
await baseCreate({ ...ctx, env: overlayEnv(ctx.env), config: overlayConfig(ctx.config) }),
|
|
168
|
+
cfg.id,
|
|
169
|
+
);
|
|
129
170
|
}
|
|
130
171
|
if (cfg.models) {
|
|
131
172
|
const models = cfg.models;
|
|
132
173
|
derived.listModels = async () => models;
|
|
133
174
|
} else if (base.listModels) {
|
|
134
175
|
const baseList = base.listModels.bind(base);
|
|
135
|
-
derived.listModels = (opts) => baseList(
|
|
176
|
+
derived.listModels = (opts) => baseList({
|
|
177
|
+
...opts,
|
|
178
|
+
env: overlayEnv(opts?.env),
|
|
179
|
+
config: overlayConfig(opts?.config),
|
|
180
|
+
});
|
|
136
181
|
}
|
|
137
182
|
if (base.listModes) {
|
|
138
183
|
const baseModes = base.listModes.bind(base);
|
|
@@ -144,10 +189,88 @@ export function defineDerivedProvider(cfg: DerivedProviderConfig): ProviderModul
|
|
|
144
189
|
derived.checkQuota = (qctx) =>
|
|
145
190
|
baseQuota({ ...qctx, env: overlayEnv(qctx?.env), config: overlayConfig(qctx?.config) });
|
|
146
191
|
}
|
|
192
|
+
if (base.probeCapabilities) {
|
|
193
|
+
const baseProbe = base.probeCapabilities.bind(base);
|
|
194
|
+
derived.probeCapabilities = (ctx) => baseProbe({
|
|
195
|
+
...ctx,
|
|
196
|
+
env: overlayEnv(ctx?.env),
|
|
197
|
+
config: overlayConfig(ctx?.config),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (base.upstreamProviders) {
|
|
201
|
+
const manager = base.upstreamProviders;
|
|
202
|
+
const runtime = (ctx?: ProviderRuntimeContext): ProviderRuntimeContext => ({
|
|
203
|
+
...ctx,
|
|
204
|
+
env: overlayEnv(ctx?.env),
|
|
205
|
+
config: overlayConfig(ctx?.config),
|
|
206
|
+
});
|
|
207
|
+
derived.upstreamProviders = {
|
|
208
|
+
list: (ctx) => manager.list(runtime(ctx)),
|
|
209
|
+
authMethods: (providerId, ctx) => manager.authMethods(providerId, runtime(ctx)),
|
|
210
|
+
setApiKey: (providerId, key, ctx) => manager.setApiKey(providerId, key, runtime(ctx)),
|
|
211
|
+
beginOAuth: (providerId, methodId, inputs, ctx) =>
|
|
212
|
+
manager.beginOAuth(providerId, methodId, inputs, runtime(ctx)),
|
|
213
|
+
completeOAuth: (flowId, code, ctx) => manager.completeOAuth(flowId, code, runtime(ctx)),
|
|
214
|
+
canDisconnect: (providerId, ctx) => manager.canDisconnect(providerId, runtime(ctx)),
|
|
215
|
+
disconnect: (providerId, ctx) => manager.disconnect(providerId, runtime(ctx)),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (base.attachSession && derived.createSession) {
|
|
219
|
+
const baseAttach = base.attachSession.bind(base);
|
|
220
|
+
derived.attachSession = async (record, opts): Promise<SessionAttachment> => {
|
|
221
|
+
assertDerivedRecord(record, cfg.id);
|
|
222
|
+
const attachment = await baseAttach(
|
|
223
|
+
{ ...record, providerType: base.type },
|
|
224
|
+
overlayAttachOptions(opts, overlayEnv, overlayConfig),
|
|
225
|
+
);
|
|
226
|
+
const derivedRecord = withProviderType(attachment.record, cfg.id)!;
|
|
227
|
+
return {
|
|
228
|
+
...attachment,
|
|
229
|
+
record: derivedRecord,
|
|
230
|
+
resume: (ctx) => derived.createSession!({
|
|
231
|
+
...ctx,
|
|
232
|
+
cwd: ctx?.cwd ?? derivedRecord.cwd ?? undefined,
|
|
233
|
+
sessionParams: derivedRecord.params,
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (base.attachHistory && derived.createSession) {
|
|
239
|
+
const baseAttachHistory = base.attachHistory.bind(base);
|
|
240
|
+
derived.attachHistory = async (record, opts): Promise<HistoryAttachment> => {
|
|
241
|
+
assertDerivedRecord(record, cfg.id);
|
|
242
|
+
const attachment = await baseAttachHistory(
|
|
243
|
+
{ ...record, providerType: base.type },
|
|
244
|
+
overlayAttachOptions(opts, overlayEnv, overlayConfig),
|
|
245
|
+
);
|
|
246
|
+
const derivedRecord = withProviderType(attachment.record, cfg.id)!;
|
|
247
|
+
return {
|
|
248
|
+
...attachment,
|
|
249
|
+
record: derivedRecord,
|
|
250
|
+
resume: (ctx) => derived.createSession!({
|
|
251
|
+
...ctx,
|
|
252
|
+
cwd: ctx?.cwd ?? derivedRecord.cwd ?? undefined,
|
|
253
|
+
sessionParams: derivedRecord.params,
|
|
254
|
+
}),
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
}
|
|
147
258
|
|
|
148
259
|
return derived;
|
|
149
260
|
}
|
|
150
261
|
|
|
262
|
+
function overlayAttachOptions(
|
|
263
|
+
opts: AttachOptions | undefined,
|
|
264
|
+
overlayEnv: (env?: Record<string, string>) => Record<string, string>,
|
|
265
|
+
overlayConfig: (config?: ProviderConfig) => ProviderConfig,
|
|
266
|
+
): AttachOptions {
|
|
267
|
+
return {
|
|
268
|
+
...opts,
|
|
269
|
+
env: overlayEnv(opts?.env),
|
|
270
|
+
config: overlayConfig(opts?.config),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
151
274
|
// ---------------------------------------------------------------------------
|
|
152
275
|
// Config-file loader
|
|
153
276
|
// ---------------------------------------------------------------------------
|
package/src/goals/controller.ts
CHANGED
|
@@ -139,6 +139,9 @@ export class GoalController {
|
|
|
139
139
|
* sentinel/loop), call `setGoal` again; hydrate does not restart the loop.
|
|
140
140
|
*/
|
|
141
141
|
hydrate(state: GoalState): void {
|
|
142
|
+
// Hydration is historical initialization, never an overwrite. A live
|
|
143
|
+
// parser event or caller-set goal that won the race remains authoritative.
|
|
144
|
+
if (this.state !== null) return;
|
|
142
145
|
this.state = state;
|
|
143
146
|
this.mode = "idle";
|
|
144
147
|
}
|
|
@@ -387,6 +390,19 @@ export class GoalController {
|
|
|
387
390
|
// re-enters onTurnSettled and continues the loop.
|
|
388
391
|
const nudge = verdict.nudge ?? defaultNudge(objective, this.iterations);
|
|
389
392
|
void this.deps.send(nudge).catch(() => {});
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (gen !== this.generation) return;
|
|
395
|
+
this.transition({
|
|
396
|
+
objective,
|
|
397
|
+
status: "blocked",
|
|
398
|
+
met: false,
|
|
399
|
+
enforced: true,
|
|
400
|
+
source: "agentex",
|
|
401
|
+
blockedReason: "sentinel_error",
|
|
402
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
403
|
+
});
|
|
404
|
+
this.mode = "idle";
|
|
405
|
+
this.sentinel = null;
|
|
390
406
|
} finally {
|
|
391
407
|
this.evaluating = false;
|
|
392
408
|
}
|
|
@@ -433,6 +449,7 @@ export class GoalController {
|
|
|
433
449
|
raw: { synthetic: "goal", ...fields },
|
|
434
450
|
};
|
|
435
451
|
if (fields.blockedReason !== undefined) event.blockedReason = fields.blockedReason;
|
|
452
|
+
if (fields.errorMessage !== undefined) event.errorMessage = fields.errorMessage;
|
|
436
453
|
if (fields.tokensUsed !== undefined) event.tokensUsed = fields.tokensUsed;
|
|
437
454
|
if (fields.timeUsedSeconds !== undefined) event.timeUsedSeconds = fields.timeUsedSeconds;
|
|
438
455
|
if (fields.tokenBudget !== undefined) event.tokenBudget = fields.tokenBudget;
|
package/src/goals/normalize.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface NormalizedGoalFields {
|
|
|
33
33
|
enforced: boolean;
|
|
34
34
|
source: GoalSource;
|
|
35
35
|
blockedReason?: GoalBlockedReason;
|
|
36
|
+
errorMessage?: string;
|
|
36
37
|
tokensUsed?: number;
|
|
37
38
|
timeUsedSeconds?: number;
|
|
38
39
|
tokenBudget?: number;
|
|
@@ -90,6 +91,9 @@ export function normalizeCodexGoalStatus(raw: string | null | undefined): {
|
|
|
90
91
|
case "budget-limited":
|
|
91
92
|
case "budget_limited":
|
|
92
93
|
case "budgetlimited": // camelCase `budgetLimited`, lowercased
|
|
94
|
+
case "usage-limited":
|
|
95
|
+
case "usage_limited":
|
|
96
|
+
case "usagelimited": // camelCase `usageLimited`, lowercased
|
|
93
97
|
return { status: "blocked", met: false, blockedReason: "budget" };
|
|
94
98
|
case "blocked":
|
|
95
99
|
return { status: "blocked", met: false, blockedReason: "needs_input" };
|
|
@@ -165,6 +169,7 @@ export function goalStateFromEvent(event: GoalStatusEvent): GoalState {
|
|
|
165
169
|
updatedAt: event.timestamp,
|
|
166
170
|
};
|
|
167
171
|
if (event.blockedReason !== undefined) state.blockedReason = event.blockedReason;
|
|
172
|
+
if (event.errorMessage !== undefined) state.errorMessage = event.errorMessage;
|
|
168
173
|
if (event.tokensUsed !== undefined) state.tokensUsed = event.tokensUsed;
|
|
169
174
|
if (event.timeUsedSeconds !== undefined) state.timeUsedSeconds = event.timeUsedSeconds;
|
|
170
175
|
if (event.tokenBudget !== undefined) state.tokenBudget = event.tokenBudget;
|
package/src/index.ts
CHANGED
|
@@ -141,6 +141,9 @@ export {
|
|
|
141
141
|
export type {
|
|
142
142
|
ProviderModule,
|
|
143
143
|
ProviderCapabilities,
|
|
144
|
+
ProviderRuntimeContext,
|
|
145
|
+
ProviderRuntimeReport,
|
|
146
|
+
CapabilityStatus,
|
|
144
147
|
AgentMode,
|
|
145
148
|
ListModesOptions,
|
|
146
149
|
ExecutionContext,
|
|
@@ -168,6 +171,11 @@ export type {
|
|
|
168
171
|
AuthRequiredReason,
|
|
169
172
|
BinaryStatus,
|
|
170
173
|
ProviderModel,
|
|
174
|
+
ListModelsOptions,
|
|
175
|
+
ProviderAuthMethod,
|
|
176
|
+
ProviderAuthFlow,
|
|
177
|
+
UpstreamProvider,
|
|
178
|
+
UpstreamProviderManager,
|
|
171
179
|
SessionContext,
|
|
172
180
|
AgentSession,
|
|
173
181
|
SendHandle,
|
|
@@ -235,6 +243,11 @@ export type {
|
|
|
235
243
|
CatchUpOptions,
|
|
236
244
|
AttachOptions,
|
|
237
245
|
SessionAttachment,
|
|
246
|
+
HistoryCheckpoint,
|
|
247
|
+
HistorySource,
|
|
248
|
+
HistoryCatchUpYield,
|
|
249
|
+
HistoryCatchUpOptions,
|
|
250
|
+
HistoryAttachment,
|
|
238
251
|
} from "./types.js";
|
|
239
252
|
|
|
240
253
|
export type {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
1
2
|
import { stat } from "node:fs/promises";
|
|
3
|
+
import * as readline from "node:readline";
|
|
2
4
|
import type {
|
|
3
5
|
AttachOptions,
|
|
4
6
|
CatchUpOptions,
|
|
@@ -19,7 +21,6 @@ import { claudeSessionCodec } from "./codec.js";
|
|
|
19
21
|
import {
|
|
20
22
|
findClaudeTranscriptBySessionId,
|
|
21
23
|
getClaudeTranscriptPath,
|
|
22
|
-
peekClaudeTranscript,
|
|
23
24
|
readClaudeTranscript,
|
|
24
25
|
} from "./transcript.js";
|
|
25
26
|
// Heavy-on-heavy is fine: this whole module loads lazily via
|
|
@@ -77,6 +78,44 @@ const EMPTY: AsyncIterable<CatchUpYield> = {
|
|
|
77
78
|
},
|
|
78
79
|
};
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Classify lifecycle boundaries from the raw transcript rather than replay's
|
|
83
|
+
* normalized events. Ordinary Claude `user` prompt records intentionally
|
|
84
|
+
* normalize to no StreamEvent, but still start a new turn. Looking only at
|
|
85
|
+
* normalized output can therefore mistake an interrupted new turn for the
|
|
86
|
+
* preceding completed one.
|
|
87
|
+
*/
|
|
88
|
+
async function classifyLastTurn(filePath: string): Promise<LastTurnStatus> {
|
|
89
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
90
|
+
stream.on("error", () => {});
|
|
91
|
+
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
92
|
+
let lastTurn: LastTurnStatus = "unknown";
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
for await (const line of lines) {
|
|
96
|
+
let raw: unknown;
|
|
97
|
+
try {
|
|
98
|
+
raw = JSON.parse(line);
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
103
|
+
const type = (raw as Record<string, unknown>)["type"];
|
|
104
|
+
if (type === "result") lastTurn = "completed";
|
|
105
|
+
else if (type === "user" || type === "assistant") lastTurn = "interrupted";
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const fsError = error as NodeJS.ErrnoException;
|
|
109
|
+
if (fsError?.code !== "ENOENT") throw error;
|
|
110
|
+
return "unknown";
|
|
111
|
+
} finally {
|
|
112
|
+
lines.close();
|
|
113
|
+
stream.destroy();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return lastTurn;
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
/**
|
|
81
120
|
* Read-only reattachment to a durable Claude session. Composition of existing
|
|
82
121
|
* primitives (codec + transcript ops + `createClaudeSession` resume) — spawns
|
|
@@ -87,6 +126,12 @@ export async function attachClaudeSession(
|
|
|
87
126
|
opts?: AttachOptions,
|
|
88
127
|
): Promise<SessionAttachment> {
|
|
89
128
|
assertSessionRecord(record);
|
|
129
|
+
if (record.providerType !== "claude") {
|
|
130
|
+
throw new MalformedSessionRecordError(
|
|
131
|
+
`claude attach requires providerType "claude"; got ${JSON.stringify(record.providerType)}`,
|
|
132
|
+
"providerType",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
90
135
|
|
|
91
136
|
// 1. Normalize params through the codec.
|
|
92
137
|
const params = claudeSessionCodec.deserialize(record.params);
|
|
@@ -113,10 +158,7 @@ export async function attachClaudeSession(
|
|
|
113
158
|
// 3. Classify how the last persisted turn ended.
|
|
114
159
|
let lastTurn: LastTurnStatus = "unknown";
|
|
115
160
|
if (transcript) {
|
|
116
|
-
|
|
117
|
-
if (lastEvent === null) lastTurn = "unknown";
|
|
118
|
-
else if (lastEvent.type === "result") lastTurn = "completed";
|
|
119
|
-
else lastTurn = "interrupted";
|
|
161
|
+
lastTurn = await classifyLastTurn(transcript.filePath);
|
|
120
162
|
}
|
|
121
163
|
|
|
122
164
|
return {
|
|
@@ -141,7 +183,7 @@ export async function attachClaudeSession(
|
|
|
141
183
|
},
|
|
142
184
|
// 5. Continue live — exactly `createSession` with the record's params.
|
|
143
185
|
resume(ctx?: SessionContext) {
|
|
144
|
-
return createClaudeSession({ ...ctx, sessionParams: params });
|
|
186
|
+
return createClaudeSession({ ...ctx, cwd: ctx?.cwd ?? cwd ?? undefined, sessionParams: params });
|
|
145
187
|
},
|
|
146
188
|
};
|
|
147
189
|
}
|
|
@@ -35,6 +35,7 @@ export const claudeProvider: ProviderModule = {
|
|
|
35
35
|
modes: false,
|
|
36
36
|
goals: claudeGoalCapability,
|
|
37
37
|
durableSessions: true,
|
|
38
|
+
durableHistory: true,
|
|
38
39
|
},
|
|
39
40
|
// Heavy machinery (execute.ts, session.ts, attach.ts) loads lazily on first
|
|
40
41
|
// use — every ProviderModule method is already async, so this is invisible to
|
|
@@ -48,6 +49,13 @@ export const claudeProvider: ProviderModule = {
|
|
|
48
49
|
transcript: claudeTranscriptOps,
|
|
49
50
|
attachSession: async (record, opts) =>
|
|
50
51
|
(await import("./attach.js")).attachClaudeSession(record, opts),
|
|
52
|
+
attachHistory: async (record, opts) => {
|
|
53
|
+
const [{ attachClaudeSession }, { historyFromSessionAttachment }] = await Promise.all([
|
|
54
|
+
import("./attach.js"),
|
|
55
|
+
import("../../sessions/history.js"),
|
|
56
|
+
]);
|
|
57
|
+
return historyFromSessionAttachment("claude", await attachClaudeSession(record, opts));
|
|
58
|
+
},
|
|
51
59
|
};
|
|
52
60
|
|
|
53
61
|
export {
|
|
@@ -216,11 +216,7 @@ export async function createClaudeSession(ctx: SessionContext): Promise<AgentSes
|
|
|
216
216
|
throw new Error("Failed to open stdio on Claude process");
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
const session = new ClaudeSessionImpl(proc, ctx, skillsDir, mcpConfigPath);
|
|
220
|
-
|
|
221
|
-
// On resume, restore an unmet native goal from the transcript (best-effort,
|
|
222
|
-
// non-blocking so session creation isn't delayed by a transcript read).
|
|
223
|
-
if (resumeId) void session.hydrateGoalFromTranscript(resumeId);
|
|
219
|
+
const session = new ClaudeSessionImpl(proc, { ...ctx, cwd }, skillsDir, mcpConfigPath);
|
|
224
220
|
|
|
225
221
|
// Wire up AbortSignal to close the session
|
|
226
222
|
if (ctx.signal) {
|
|
@@ -231,6 +227,10 @@ export async function createClaudeSession(ctx: SessionContext): Promise<AgentSes
|
|
|
231
227
|
}
|
|
232
228
|
}
|
|
233
229
|
|
|
230
|
+
// Finish historical hydration before exposing the session. This prevents a
|
|
231
|
+
// late transcript read from overwriting a goal the caller sets immediately.
|
|
232
|
+
if (resumeId && session.state !== "closed") await session.hydrateGoalFromTranscript(resumeId);
|
|
233
|
+
|
|
234
234
|
return session;
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -307,6 +307,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
307
307
|
/** Tail state for observing native goal_status (transcript-only, not on stdout). */
|
|
308
308
|
private _goalScanOffset = 0;
|
|
309
309
|
private _goalPoll: ReturnType<typeof setInterval> | null = null;
|
|
310
|
+
private readonly cwd: string;
|
|
310
311
|
|
|
311
312
|
constructor(
|
|
312
313
|
private readonly proc: ChildProcess,
|
|
@@ -314,6 +315,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
314
315
|
private readonly skillsDir: string | null,
|
|
315
316
|
private readonly mcpConfigPath: string | null = null,
|
|
316
317
|
) {
|
|
318
|
+
this.cwd = ctx.cwd ?? process.cwd();
|
|
317
319
|
this._goals = new GoalController({
|
|
318
320
|
providerType: "claude",
|
|
319
321
|
capability: claudeGoalCapability,
|
|
@@ -384,6 +386,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
384
386
|
|
|
385
387
|
get sessionId(): string | null { return this._sessionId; }
|
|
386
388
|
get state(): SessionState { return this._state; }
|
|
389
|
+
private isClosed(): boolean { return this._state === "closed"; }
|
|
387
390
|
|
|
388
391
|
/**
|
|
389
392
|
* Durable identity for persistence + later `attachSession`. Null until Claude
|
|
@@ -392,7 +395,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
392
395
|
*/
|
|
393
396
|
describe(): SessionRecord | null {
|
|
394
397
|
if (!this._sessionId) return null;
|
|
395
|
-
const cwd = this.
|
|
398
|
+
const cwd = this.cwd;
|
|
396
399
|
const params = claudeSessionCodec.serialize({
|
|
397
400
|
sessionId: this._sessionId,
|
|
398
401
|
...(cwd ? { cwd } : {}),
|
|
@@ -1144,7 +1147,7 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
1144
1147
|
try {
|
|
1145
1148
|
const found = await claudeTranscriptOps.find({
|
|
1146
1149
|
sessionId,
|
|
1147
|
-
cwd: this.
|
|
1150
|
+
cwd: this.cwd,
|
|
1148
1151
|
});
|
|
1149
1152
|
if (found) this._transcriptPath = found.filePath;
|
|
1150
1153
|
} catch { /* best effort */ } finally {
|
|
@@ -1166,20 +1169,22 @@ export class ClaudeSessionImpl implements AgentSession {
|
|
|
1166
1169
|
*/
|
|
1167
1170
|
async hydrateGoalFromTranscript(sessionId: string): Promise<void> {
|
|
1168
1171
|
try {
|
|
1172
|
+
if (this.isClosed()) return;
|
|
1169
1173
|
const found = await claudeTranscriptOps.find({
|
|
1170
1174
|
sessionId,
|
|
1171
|
-
cwd: this.
|
|
1175
|
+
cwd: this.cwd,
|
|
1172
1176
|
});
|
|
1173
|
-
if (!found) return;
|
|
1177
|
+
if (!found || this.isClosed()) return;
|
|
1174
1178
|
this._transcriptPath = found.filePath;
|
|
1175
1179
|
const events: StreamEvent[] = [];
|
|
1176
1180
|
let endOffset = 0;
|
|
1177
1181
|
for await (const { event, offset } of claudeTranscriptOps.read({ filePath: found.filePath })) {
|
|
1182
|
+
if (this.isClosed()) return;
|
|
1178
1183
|
endOffset = offset;
|
|
1179
1184
|
if (event.type === "goal_status") events.push(event);
|
|
1180
1185
|
}
|
|
1181
1186
|
const last = latestGoalFromEvents(events);
|
|
1182
|
-
if (last && !isTerminalGoalStatus(last.status)) {
|
|
1187
|
+
if (!this.isClosed() && last && !isTerminalGoalStatus(last.status)) {
|
|
1183
1188
|
// Start observing from the END of the historical transcript so the poller
|
|
1184
1189
|
// surfaces only post-resume transitions (we already hydrated the state).
|
|
1185
1190
|
this._goalScanOffset = endOffset;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { getRuntimeHomeEnvVar } from "../../utils/runtime-homes.js";
|
|
17
17
|
import { codexSessionCodec } from "./codec.js";
|
|
18
18
|
import {
|
|
19
|
+
type CodexTranscriptLine,
|
|
19
20
|
getCodexTranscriptPath,
|
|
20
21
|
peekCodexTranscript,
|
|
21
22
|
readCodexCwd,
|
|
@@ -37,6 +38,44 @@ const EMPTY: AsyncIterable<CatchUpYield> = {
|
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
40
|
|
|
41
|
+
function isTurnBoundary(line: CodexTranscriptLine, sessionId: string): boolean {
|
|
42
|
+
const payloadType = typeof line.payload?.["type"] === "string" ? line.payload["type"] : null;
|
|
43
|
+
|
|
44
|
+
// These raw records start a turn but intentionally normalize to no replay
|
|
45
|
+
// event. They must still supersede an earlier task_complete when deciding
|
|
46
|
+
// whether the latest persisted turn was interrupted.
|
|
47
|
+
if (line.type === "event_msg" && (payloadType === "task_started" || payloadType === "user_message")) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (line.type === "response_item" && payloadType === "message" && line.payload?.["role"] === "user") {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (line.type === "message" && line.raw["role"] === "user") return true;
|
|
54
|
+
|
|
55
|
+
return codexLineToStreamEvents(line, { sessionId }).some((event) => ![
|
|
56
|
+
"system",
|
|
57
|
+
"permission_mode",
|
|
58
|
+
"rate_limit",
|
|
59
|
+
"goal_status",
|
|
60
|
+
"unknown",
|
|
61
|
+
].includes(event.type));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function latestTurnBoundary(
|
|
65
|
+
filePath: string,
|
|
66
|
+
sessionId: string,
|
|
67
|
+
): Promise<CodexTranscriptLine | null> {
|
|
68
|
+
const { lastEvent } = await peekCodexTranscript(filePath, {
|
|
69
|
+
accept: (line) => isTurnBoundary(line, sessionId),
|
|
70
|
+
});
|
|
71
|
+
if (lastEvent) return lastEvent;
|
|
72
|
+
let latest: CodexTranscriptLine | null = null;
|
|
73
|
+
for await (const { event } of readCodexTranscript({ filePath })) {
|
|
74
|
+
if (isTurnBoundary(event, sessionId)) latest = event;
|
|
75
|
+
}
|
|
76
|
+
return latest;
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
/**
|
|
41
80
|
* Read-only reattachment to a durable Codex session. Same skeleton as Claude,
|
|
42
81
|
* with two deltas: classify via the rollout's last line, and replay through
|
|
@@ -48,6 +87,12 @@ export async function attachCodexSession(
|
|
|
48
87
|
opts?: AttachOptions,
|
|
49
88
|
): Promise<SessionAttachment> {
|
|
50
89
|
assertSessionRecord(record);
|
|
90
|
+
if (record.providerType !== "codex") {
|
|
91
|
+
throw new MalformedSessionRecordError(
|
|
92
|
+
`codex attach requires providerType "codex"; got ${JSON.stringify(record.providerType)}`,
|
|
93
|
+
"providerType",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
51
96
|
|
|
52
97
|
// 1. Normalize params through the codec.
|
|
53
98
|
const params = codexSessionCodec.deserialize(record.params);
|
|
@@ -85,9 +130,9 @@ export async function attachCodexSession(
|
|
|
85
130
|
// 3. Classify how the last persisted turn ended.
|
|
86
131
|
let lastTurn: LastTurnStatus = "unknown";
|
|
87
132
|
if (transcript) {
|
|
88
|
-
const
|
|
133
|
+
const lastEvent = await latestTurnBoundary(transcript.filePath, sessionId);
|
|
89
134
|
if (lastEvent === null) lastTurn = "unknown";
|
|
90
|
-
else if (lastEvent
|
|
135
|
+
else if (codexLineToStreamEvents(lastEvent, { sessionId }).some((event) => event.type === "result"))
|
|
91
136
|
lastTurn = "completed";
|
|
92
137
|
else lastTurn = "interrupted";
|
|
93
138
|
}
|
|
@@ -117,7 +162,7 @@ export async function attachCodexSession(
|
|
|
117
162
|
},
|
|
118
163
|
// 5. Continue live — exactly `createSession` with the record's params.
|
|
119
164
|
resume(ctx?: SessionContext) {
|
|
120
|
-
return createCodexSession({ ...ctx, sessionParams: params });
|
|
165
|
+
return createCodexSession({ ...ctx, cwd: ctx?.cwd ?? cwd ?? undefined, sessionParams: params });
|
|
121
166
|
},
|
|
122
167
|
};
|
|
123
168
|
}
|
|
@@ -23,6 +23,7 @@ export const codexProvider: ProviderModule = {
|
|
|
23
23
|
modes: true,
|
|
24
24
|
goals: codexGoalCapability,
|
|
25
25
|
durableSessions: true,
|
|
26
|
+
durableHistory: true,
|
|
26
27
|
},
|
|
27
28
|
// Heavy machinery (execute.ts, session.ts, modes.ts, attach.ts) loads lazily
|
|
28
29
|
// on first use — every ProviderModule method is already async.
|
|
@@ -35,6 +36,13 @@ export const codexProvider: ProviderModule = {
|
|
|
35
36
|
listModes: async (opts) => (await import("./modes.js")).listCodexModes(opts),
|
|
36
37
|
attachSession: async (record, opts) =>
|
|
37
38
|
(await import("./attach.js")).attachCodexSession(record, opts),
|
|
39
|
+
attachHistory: async (record, opts) => {
|
|
40
|
+
const [{ attachCodexSession }, { historyFromSessionAttachment }] = await Promise.all([
|
|
41
|
+
import("./attach.js"),
|
|
42
|
+
import("../../sessions/history.js"),
|
|
43
|
+
]);
|
|
44
|
+
return historyFromSessionAttachment("codex", await attachCodexSession(record, opts));
|
|
45
|
+
},
|
|
38
46
|
};
|
|
39
47
|
|
|
40
48
|
export {
|
|
@@ -619,6 +619,11 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
619
619
|
input: [{ type: "text", text: message }],
|
|
620
620
|
};
|
|
621
621
|
if (this._threadId) turnParams["threadId"] = this._threadId;
|
|
622
|
+
// Codex app-server accepts reasoning effort as a turn/start override. The
|
|
623
|
+
// selected value becomes the default for later turns on this thread, so
|
|
624
|
+
// forwarding the session config here works for both fresh and resumed
|
|
625
|
+
// sessions without mutating the user's global config.toml.
|
|
626
|
+
if (this.ctx.config?.effort) turnParams["effort"] = this.ctx.config.effort;
|
|
622
627
|
|
|
623
628
|
let resolveFn!: (r: TurnResult) => void;
|
|
624
629
|
let rejectFn!: (e: Error) => void;
|
|
@@ -415,7 +415,10 @@ export interface CodexPeekResult {
|
|
|
415
415
|
* Cheap drift-check: reads up to {@link PEEK_TAIL_BYTES} from the tail,
|
|
416
416
|
* walks back to the last parseable line, returns it plus the file size.
|
|
417
417
|
*/
|
|
418
|
-
export async function peekCodexTranscript(
|
|
418
|
+
export async function peekCodexTranscript(
|
|
419
|
+
filePath: string,
|
|
420
|
+
options: { accept?: (event: CodexTranscriptLine) => boolean } = {},
|
|
421
|
+
): Promise<CodexPeekResult> {
|
|
419
422
|
let size: number;
|
|
420
423
|
try {
|
|
421
424
|
const s = await stat(filePath);
|
|
@@ -452,6 +455,7 @@ export async function peekCodexTranscript(filePath: string): Promise<CodexPeekRe
|
|
|
452
455
|
if (!trimmed) continue;
|
|
453
456
|
const parsed = parseCodexLine(trimmed);
|
|
454
457
|
if (!parsed) continue;
|
|
458
|
+
if (options.accept && !options.accept(parsed)) continue;
|
|
455
459
|
return { lastEvent: parsed, size };
|
|
456
460
|
}
|
|
457
461
|
|