@agentex/agent 0.0.30 → 0.0.31

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +71 -2
  3. package/dist/derived.d.ts.map +1 -1
  4. package/dist/derived.js +35 -0
  5. package/dist/derived.js.map +1 -1
  6. package/dist/history/index.d.ts +1 -1
  7. package/dist/history/index.d.ts.map +1 -1
  8. package/dist/history/types.d.ts +74 -1
  9. package/dist/history/types.d.ts.map +1 -1
  10. package/dist/history/types.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/providers/opencode/history.d.ts +22 -8
  15. package/dist/providers/opencode/history.d.ts.map +1 -1
  16. package/dist/providers/opencode/history.js +119 -18
  17. package/dist/providers/opencode/history.js.map +1 -1
  18. package/dist/providers/opencode/index.d.ts.map +1 -1
  19. package/dist/providers/opencode/index.js +17 -0
  20. package/dist/providers/opencode/index.js.map +1 -1
  21. package/dist/providers/opencode/probe.d.ts.map +1 -1
  22. package/dist/providers/opencode/probe.js +2 -0
  23. package/dist/providers/opencode/probe.js.map +1 -1
  24. package/dist/providers/opencode/saved-history.d.ts +20 -0
  25. package/dist/providers/opencode/saved-history.d.ts.map +1 -0
  26. package/dist/providers/opencode/saved-history.js +361 -0
  27. package/dist/providers/opencode/saved-history.js.map +1 -0
  28. package/dist/types.d.ts +4 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/types.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/derived.ts +39 -0
  33. package/src/history/index.ts +10 -0
  34. package/src/history/types.ts +90 -1
  35. package/src/index.ts +11 -1
  36. package/src/providers/opencode/history.ts +156 -23
  37. package/src/providers/opencode/index.ts +17 -0
  38. package/src/providers/opencode/probe.ts +4 -0
  39. package/src/providers/opencode/saved-history.ts +467 -0
  40. package/src/types.ts +4 -0
@@ -1,4 +1,93 @@
1
- import type { BaseStreamEventFields, StreamEvent } from "../types.js";
1
+ import type {
2
+ BaseStreamEventFields,
3
+ HistoryCheckpoint,
4
+ ProviderRuntimeContext,
5
+ StreamEvent,
6
+ } from "../types.js";
7
+
8
+ /**
9
+ * Provider-neutral discovery options for persisted sessions. Unlike
10
+ * `LocalHistoryDiscoverOptions`, these options do not assume a filesystem
11
+ * source. Providers may satisfy them through a local authenticated service.
12
+ */
13
+ export interface SavedHistoryDiscoverOptions extends ProviderRuntimeContext {
14
+ /** Optional provider-session directory filter. `cwd` is runtime context only. */
15
+ directory?: string;
16
+ includeArchived?: boolean;
17
+ mainSessionsOnly?: boolean;
18
+ requireUserMessage?: boolean;
19
+ limit?: number;
20
+ }
21
+
22
+ export interface SavedHistoryProbeOptions extends ProviderRuntimeContext {
23
+ limit?: number;
24
+ }
25
+
26
+ export interface SavedHistoryProbeResult {
27
+ providerType: string;
28
+ /** The provider-owned history source could be reached. */
29
+ sourceAvailable: boolean;
30
+ historyAvailable: boolean;
31
+ /** A bounded source count, not necessarily an eligible-session count. */
32
+ approximateCount?: number;
33
+ }
34
+
35
+ export type SavedHistoryArchiveState = "active" | "archived" | "unknown";
36
+
37
+ /**
38
+ * Serializable metadata for a provider-owned persisted session. Storage
39
+ * details such as transcript paths, byte offsets, and database layouts are
40
+ * deliberately absent.
41
+ */
42
+ export interface SavedHistorySession {
43
+ version: 1;
44
+ providerType: string;
45
+ externalSessionId: string;
46
+ cwd: string | null;
47
+ title: string | null;
48
+ startedAt: string | null;
49
+ updatedAt: string;
50
+ branch: string | null;
51
+ gitOriginUrl: string | null;
52
+ archiveState: SavedHistoryArchiveState;
53
+ hasUserMessage: boolean;
54
+ }
55
+
56
+ export type SavedHistoryUserEvent = {
57
+ type: "user";
58
+ text: string;
59
+ } & BaseStreamEventFields;
60
+
61
+ export type SavedHistoryEvent = StreamEvent | SavedHistoryUserEvent;
62
+
63
+ export interface SavedHistoryYield {
64
+ event: SavedHistoryEvent & { eventId: string };
65
+ /** Opaque provider-owned bookmark. Persist only after the event commits. */
66
+ checkpoint: HistoryCheckpoint;
67
+ eventId: string;
68
+ /** Disambiguates normalized events that share one provider source part. */
69
+ partIndex: number;
70
+ }
71
+
72
+ export interface SavedHistoryReadOptions extends ProviderRuntimeContext {
73
+ after?: HistoryCheckpoint;
74
+ mode?: "incremental" | "bounded_full_resync";
75
+ }
76
+
77
+ /**
78
+ * Discover and read provider-owned saved sessions when their ids are not yet
79
+ * known. This is the provider-neutral import/synchronization surface. It is
80
+ * separate from `attachHistory`, which starts from a known `SessionRecord`,
81
+ * and from the file-specific `LocalHistoryOps` compatibility API.
82
+ */
83
+ export interface SavedHistoryOps {
84
+ probe(options?: SavedHistoryProbeOptions): Promise<SavedHistoryProbeResult>;
85
+ discover(options?: SavedHistoryDiscoverOptions): AsyncIterable<SavedHistorySession>;
86
+ read(
87
+ session: SavedHistorySession,
88
+ options?: SavedHistoryReadOptions,
89
+ ): AsyncIterable<SavedHistoryYield>;
90
+ }
2
91
 
3
92
  export interface LocalHistoryDiscoverOptions {
4
93
  includeArchived?: boolean;
package/src/index.ts CHANGED
@@ -206,9 +206,19 @@ export type {
206
206
  GoalSentinelContext,
207
207
  } from "./types.js";
208
208
 
209
- // Local persisted session discovery. Runtime errors are exported from the
209
+ // Persisted session discovery. Runtime errors are exported from the
210
210
  // `@agentex/agent/history` subpath to keep the root barrel lazy.
211
211
  export type {
212
+ SavedHistoryArchiveState,
213
+ SavedHistoryDiscoverOptions,
214
+ SavedHistoryEvent,
215
+ SavedHistoryOps,
216
+ SavedHistoryProbeOptions,
217
+ SavedHistoryProbeResult,
218
+ SavedHistoryReadOptions,
219
+ SavedHistorySession,
220
+ SavedHistoryUserEvent,
221
+ SavedHistoryYield,
212
222
  LocalHistoryArchiveState,
213
223
  LocalHistoryDiscoverOptions,
214
224
  LocalHistoryErrorCode,
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  import type {
2
4
  AttachOptions,
3
5
  HistoryAttachment,
@@ -6,8 +8,9 @@ import type {
6
8
  HistoryCheckpoint,
7
9
  LastTurnStatus,
8
10
  SessionRecord,
9
- StreamEvent,
10
11
  } from "../../types.js";
12
+ import type { SavedHistoryEvent, SavedHistoryUserEvent } from "../../history/types.js";
13
+ import { meaningfulHumanText } from "../../history/fs.js";
11
14
  import { assertSessionRecord, createSessionRecord, MalformedSessionRecordError } from "../../sessions/record.js";
12
15
  import { acquireOpenCodeRuntime } from "./runtime.js";
13
16
  import { opencodeSessionCodec } from "./codec.js";
@@ -18,7 +21,7 @@ const PAGE_SIZE = 100;
18
21
  const MAX_PAGES = 100;
19
22
  const MAX_MESSAGES = 10_000;
20
23
  const MAX_BYTES = 25 * 1024 * 1024;
21
- const CHECKPOINT_KIND = "opencode:message-part:v1";
24
+ const CHECKPOINT_KIND = "opencode:message-part:v2";
22
25
 
23
26
  export class OpenCodeHistoryCheckpointNotFoundError extends Error {
24
27
  readonly code = "history_checkpoint_not_found";
@@ -36,17 +39,39 @@ export class OpenCodeHistoryResyncLimitError extends Error {
36
39
  }
37
40
  }
38
41
 
42
+ export class OpenCodeHistorySourceMissingError extends Error {
43
+ readonly code = "source_missing";
44
+
45
+ constructor(sessionId: string) {
46
+ super(`OpenCode saved session ${JSON.stringify(sessionId)} no longer exists`);
47
+ this.name = "OpenCodeHistorySourceMissingError";
48
+ }
49
+ }
50
+
39
51
  interface DecodedCheckpoint {
40
52
  messageId: string;
41
53
  partId: string;
42
54
  ordinal: number | null;
55
+ messageRevision: string;
43
56
  }
44
57
 
45
- export interface MessageEnvelope {
58
+ export interface MessageEnvelope extends Record<string, unknown> {
46
59
  info?: Record<string, unknown>;
47
60
  parts?: unknown[];
48
61
  }
49
62
 
63
+ interface HistoricalEvent {
64
+ event: SavedHistoryEvent;
65
+ partId: string;
66
+ }
67
+
68
+ export interface CollectedHistoryEvent {
69
+ event: SavedHistoryEvent;
70
+ checkpoint: HistoryCheckpoint;
71
+ eventId: string | null;
72
+ partIndex: number;
73
+ }
74
+
50
75
  function rec(value: unknown): Record<string, unknown> | null {
51
76
  return value && typeof value === "object" && !Array.isArray(value)
52
77
  ? value as Record<string, unknown>
@@ -57,35 +82,118 @@ function string(value: unknown): string | null {
57
82
  return typeof value === "string" && value.length > 0 ? value : null;
58
83
  }
59
84
 
85
+ function historicalTimestamp(info: Record<string, unknown>): string {
86
+ const created = rec(info["time"])?.["created"];
87
+ if (typeof created === "number" && Number.isFinite(created)) {
88
+ const timestamp = new Date(created);
89
+ if (!Number.isNaN(timestamp.getTime())) return timestamp.toISOString();
90
+ }
91
+ return new Date(0).toISOString();
92
+ }
93
+
60
94
  function decodeCheckpoint(checkpoint: HistoryCheckpoint | undefined): DecodedCheckpoint | null {
61
95
  if (!checkpoint) return null;
62
96
  if (checkpoint.kind !== CHECKPOINT_KIND) throw new OpenCodeHistoryCheckpointNotFoundError();
63
97
  const value = rec(checkpoint.value);
64
98
  const messageId = string(value?.["messageId"]);
65
99
  const partId = string(value?.["partId"]);
66
- if (!messageId || !partId) throw new OpenCodeHistoryCheckpointNotFoundError();
100
+ const messageRevision = string(value?.["messageRevision"]);
101
+ if (!messageId || !partId || !messageRevision) throw new OpenCodeHistoryCheckpointNotFoundError();
67
102
  const ordinal = typeof value?.["ordinal"] === "number" && Number.isInteger(value["ordinal"])
68
103
  ? value["ordinal"] as number
69
104
  : null;
70
- return { messageId, partId, ordinal };
105
+ return { messageId, partId, ordinal, messageRevision };
106
+ }
107
+
108
+ function checkpoint(
109
+ messageId: string,
110
+ partId: string,
111
+ ordinal: number,
112
+ messageRevision: string,
113
+ ): HistoryCheckpoint {
114
+ return {
115
+ kind: CHECKPOINT_KIND,
116
+ value: { messageId, partId, ordinal, messageRevision },
117
+ };
118
+ }
119
+
120
+ function canonicalJson(value: unknown): unknown {
121
+ if (Array.isArray(value)) return value.map(canonicalJson);
122
+ const record = rec(value);
123
+ if (record) {
124
+ return Object.fromEntries(
125
+ Object.keys(record).sort().map((key) => [key, canonicalJson(record[key])]),
126
+ );
127
+ }
128
+ return value;
71
129
  }
72
130
 
73
- function checkpoint(messageId: string, partId: string, ordinal: number): HistoryCheckpoint {
74
- return { kind: CHECKPOINT_KIND, value: { messageId, partId, ordinal } };
131
+ function messageRevision(message: MessageEnvelope): string {
132
+ return createHash("sha256")
133
+ .update(JSON.stringify(canonicalJson(message)))
134
+ .digest("hex");
75
135
  }
76
136
 
77
- export function historicalEvents(message: MessageEnvelope, sessionId: string): Array<{ event: StreamEvent; partId: string }> {
137
+ function userTextPart(part: Record<string, unknown>): string | null {
138
+ if (part["synthetic"] === true || part["ignored"] === true) return null;
139
+ if (part["type"] === "text") return string(part["text"]);
140
+ if (part["type"] === "subtask") return string(part["prompt"]);
141
+ return null;
142
+ }
143
+
144
+ function historicalUserEvent(
145
+ message: MessageEnvelope,
146
+ sessionId: string,
147
+ timestamp: string,
148
+ ): HistoricalEvent | null {
78
149
  const info = message.info ?? {};
79
- if (info["role"] !== "assistant") return [];
80
150
  const messageId = string(info["id"]);
81
- const timestamp = new Date(
82
- typeof rec(info["time"])?.["created"] === "number"
83
- ? rec(info["time"])!["created"] as number
84
- : Date.now(),
85
- ).toISOString();
151
+ const texts: string[] = [];
152
+ let partId: string | null = null;
153
+ for (const rawPart of Array.isArray(message.parts) ? message.parts : []) {
154
+ const part = rec(rawPart);
155
+ const text = part ? userTextPart(part) : null;
156
+ if (!part || !text) continue;
157
+ const candidatePartId = string(part["id"]);
158
+ if (!candidatePartId) continue;
159
+ texts.push(text);
160
+ partId = candidatePartId;
161
+ }
162
+ const text = meaningfulHumanText(texts.join("\n\n"));
163
+ if (!text || !partId) return null;
164
+ const eventId = messageId ?? partId;
165
+ const event: SavedHistoryUserEvent = {
166
+ type: "user",
167
+ text,
168
+ timestamp,
169
+ providerType: "opencode",
170
+ sessionId,
171
+ messageId,
172
+ eventId,
173
+ turnId: null,
174
+ parentToolCallId: null,
175
+ raw: message,
176
+ };
177
+ return { event, partId };
178
+ }
179
+
180
+ export function historicalEvents(
181
+ message: MessageEnvelope,
182
+ sessionId: string,
183
+ options: { includeUserMessages?: boolean } = {},
184
+ ): HistoricalEvent[] {
185
+ const info = rec(message.info) ?? {};
186
+ const messageId = string(info["id"]);
187
+ const timestamp = historicalTimestamp(info);
188
+ if (info["role"] === "user") {
189
+ if (!options.includeUserMessages) return [];
190
+ const user = historicalUserEvent(message, sessionId, timestamp);
191
+ return user ? [user] : [];
192
+ }
193
+ if (info["role"] !== "assistant") return [];
86
194
  const base: OcBaseInfo = { provider: "opencode", sessionId, timestamp };
87
- const events: Array<{ event: StreamEvent; partId: string }> = [];
88
- for (const rawPart of message.parts ?? []) {
195
+ const events: HistoricalEvent[] = [];
196
+ for (const rawPart of Array.isArray(message.parts) ? message.parts : []) {
89
197
  const part = rec(rawPart);
90
198
  const partId = string(part?.["id"]);
91
199
  if (!part || !partId) continue;
@@ -99,7 +207,8 @@ export function historicalEvents(message: MessageEnvelope, sessionId: string): A
99
207
  if (event) events.push({ event, partId });
100
208
  }
101
209
  const lastPartId = events.at(-1)?.partId;
102
- if (messageId && lastPartId) {
210
+ const finished = string(info["finish"]) !== null || info["error"] != null;
211
+ if (messageId && lastPartId && finished) {
103
212
  const error = info["error"] != null;
104
213
  events.push({
105
214
  partId: lastPartId,
@@ -130,7 +239,11 @@ export async function collectHistory(
130
239
  client: import("./client.js").OpenCodeClient,
131
240
  sessionId: string,
132
241
  options: HistoryCatchUpOptions | undefined,
133
- ): Promise<Array<{ event: StreamEvent; checkpoint: HistoryCheckpoint; eventId: string | null }>> {
242
+ mapping: {
243
+ includeUserMessages?: boolean;
244
+ missingSession?: "empty" | "error";
245
+ } = {},
246
+ ): Promise<CollectedHistoryEvent[]> {
134
247
  const after = options?.mode === "bounded_full_resync" ? null : decodeCheckpoint(options?.after);
135
248
  const pages: MessageEnvelope[][] = [];
136
249
  let before: string | null = null;
@@ -142,7 +255,12 @@ export async function collectHistory(
142
255
  const query = new URLSearchParams({ limit: String(PAGE_SIZE) });
143
256
  if (before) query.set("before", before);
144
257
  const response = await client.request(`/session/${encodeURIComponent(sessionId)}/message?${query}`);
145
- if (response.status === 404) return [];
258
+ if (response.status === 404) {
259
+ if (mapping.missingSession === "error") {
260
+ throw new OpenCodeHistorySourceMissingError(sessionId);
261
+ }
262
+ return [];
263
+ }
146
264
  if (!response.ok) throw new Error(`OpenCode history request failed (${response.status})`);
147
265
  const text = await response.text();
148
266
  collectedBytes += Buffer.byteLength(text);
@@ -171,13 +289,21 @@ export async function collectHistory(
171
289
  }
172
290
 
173
291
  const messages = pages.reverse().flat();
174
- const output: Array<{ event: StreamEvent; checkpoint: HistoryCheckpoint; eventId: string | null }> = [];
292
+ const output: CollectedHistoryEvent[] = [];
175
293
  let passedCheckpoint = after === null;
176
294
  let foundPart = after === null;
177
295
  for (const message of messages) {
178
296
  const messageId = string(message.info?.["id"]);
179
297
  if (!messageId) continue;
180
- const events = historicalEvents(message, sessionId);
298
+ const revision = messageRevision(message);
299
+ if (
300
+ after
301
+ && messageId === after.messageId
302
+ && revision !== after.messageRevision
303
+ ) {
304
+ throw new OpenCodeHistoryCheckpointNotFoundError();
305
+ }
306
+ const events = historicalEvents(message, sessionId, mapping);
181
307
  const partOrdinals = new Map<string, number>();
182
308
  for (const item of events) {
183
309
  const ordinal = partOrdinals.get(item.partId) ?? 0;
@@ -204,8 +330,9 @@ export async function collectHistory(
204
330
  }
205
331
  output.push({
206
332
  event: item.event,
207
- checkpoint: checkpoint(messageId, item.partId, ordinal),
333
+ checkpoint: checkpoint(messageId, item.partId, ordinal, revision),
208
334
  eventId: item.event.eventId,
335
+ partIndex: ordinal,
209
336
  });
210
337
  }
211
338
  if (foundPart && messageId === after?.messageId) passedCheckpoint = true;
@@ -256,7 +383,13 @@ export async function attachOpenCodeHistory(
256
383
  catchUp(options) {
257
384
  return {
258
385
  async *[Symbol.asyncIterator](): AsyncIterator<HistoryCatchUpYield> {
259
- for (const yielded of await collectHistory(runtime.server.client, sessionId, options)) yield yielded;
386
+ for (const yielded of await collectHistory(runtime.server.client, sessionId, options)) {
387
+ // Known-session catch-up deliberately excludes user prompts. The
388
+ // host already owns them. `savedHistory.read()` opts in to users
389
+ // when importing a conversation the host did not create.
390
+ const event = yielded.event;
391
+ if (event.type !== "user") yield { ...yielded, event };
392
+ }
260
393
  },
261
394
  };
262
395
  },
@@ -20,6 +20,7 @@ export const opencodeProvider: ProviderModule = {
20
20
  modes: true,
21
21
  goals: EMULATED_GOAL_CAPABILITY,
22
22
  durableHistory: true,
23
+ savedHistory: true,
23
24
  resume: true,
24
25
  modelVariants: true,
25
26
  permissionRequests: true,
@@ -50,4 +51,20 @@ export const opencodeProvider: ProviderModule = {
50
51
  disconnect: (providerId, ctx) => import("./manager.js").then((m) => m.openCodeUpstreamProviders.disconnect(providerId, ctx)),
51
52
  },
52
53
  attachHistory: (record, options) => import("./history.js").then((m) => m.attachOpenCodeHistory(record, options)),
54
+ savedHistory: {
55
+ probe: (options) => import("./saved-history.js")
56
+ .then((module) => module.openCodeSavedHistory.probe(options)),
57
+ discover: (options) => ({
58
+ async *[Symbol.asyncIterator]() {
59
+ const module = await import("./saved-history.js");
60
+ yield* module.openCodeSavedHistory.discover(options);
61
+ },
62
+ }),
63
+ read: (session, options) => ({
64
+ async *[Symbol.asyncIterator]() {
65
+ const module = await import("./saved-history.js");
66
+ yield* module.openCodeSavedHistory.read(session, options);
67
+ },
68
+ }),
69
+ },
53
70
  };
@@ -74,6 +74,10 @@ export async function probeOpenCodeCapabilities(
74
74
  questionRequests: capability(has("/question", "get") && has("/question/{requestID}/reply", "post")),
75
75
  upstreamProviderDisconnect: capability(supportsDisconnect),
76
76
  durableHistory: capability(has("/session/{sessionID}/message", "get")),
77
+ savedHistory: capability(
78
+ (has("/experimental/session", "get") || has("/session", "get"))
79
+ && has("/session/{sessionID}/message", "get"),
80
+ ),
77
81
  planMode: capability(agents.some((value) => {
78
82
  const agent = rec(value);
79
83
  return agent["name"] === "plan" || agent["id"] === "plan";