@poncho-ai/harness 0.53.0 → 0.57.0
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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +79 -0
- package/dist/index.d.ts +216 -2
- package/dist/index.js +670 -27
- package/package.json +1 -1
- package/src/compaction.ts +206 -13
- package/src/index.ts +18 -0
- package/src/orchestrator/entries-dual-write.ts +265 -0
- package/src/orchestrator/index.ts +7 -0
- package/src/orchestrator/orchestrator.ts +179 -13
- package/src/orchestrator/run-conversation-turn.ts +108 -0
- package/src/state.ts +56 -0
- package/src/storage/engine.ts +18 -0
- package/src/storage/entries.ts +217 -0
- package/src/storage/memory-engine.ts +40 -0
- package/src/storage/schema.ts +30 -0
- package/src/storage/sql-dialect.ts +112 -0
- package/src/storage/store-adapters.ts +8 -0
- package/test/compaction.test.ts +274 -0
- package/test/entries-dual-write.test.ts +172 -0
- package/test/entries-store.test.ts +165 -0
- package/test/entries.test.ts +125 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getTextContent, type AgentEvent, type Message } from "@poncho-ai/sdk";
|
|
1
|
+
import { createLogger, getTextContent, type AgentEvent, type Message } from "@poncho-ai/sdk";
|
|
2
2
|
import type { Conversation, ConversationStore, PendingSubagentResult } from "../state.js";
|
|
3
3
|
import type { AgentHarness } from "../harness.js";
|
|
4
4
|
import type { TelemetryEmitter } from "../telemetry.js";
|
|
@@ -27,6 +27,17 @@ import {
|
|
|
27
27
|
CALLBACK_LOCK_STALE_MS,
|
|
28
28
|
STALE_SUBAGENT_THRESHOLD_MS,
|
|
29
29
|
} from "./subagents.js";
|
|
30
|
+
import {
|
|
31
|
+
appendEntriesSafe,
|
|
32
|
+
assistantAmendmentEntry,
|
|
33
|
+
assistantMessageEntry,
|
|
34
|
+
callbackStartedEntry,
|
|
35
|
+
subagentResultEntry,
|
|
36
|
+
userMessageEntry,
|
|
37
|
+
verifyEntriesParity,
|
|
38
|
+
} from "./entries-dual-write.js";
|
|
39
|
+
|
|
40
|
+
const dualWriteLog = createLogger("orchestrator:entries");
|
|
30
41
|
|
|
31
42
|
// ── Subagent result extraction ──
|
|
32
43
|
|
|
@@ -491,6 +502,11 @@ export class AgentOrchestrator {
|
|
|
491
502
|
if (!checkpointedRun) {
|
|
492
503
|
const conv = await this.conversationStore.get(conversationId);
|
|
493
504
|
if (conv) {
|
|
505
|
+
// Track which dual-write branch the blob took: an in-place merge onto
|
|
506
|
+
// the previous assistant bubble (→ assistant_amendment) or a fresh
|
|
507
|
+
// bubble (→ assistant_message).
|
|
508
|
+
let amendmentText: string | undefined;
|
|
509
|
+
let pushedAssistant: Message | undefined;
|
|
494
510
|
const hasAssistantContent =
|
|
495
511
|
draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0;
|
|
496
512
|
if (hasAssistantContent) {
|
|
@@ -519,15 +535,14 @@ export class AgentOrchestrator {
|
|
|
519
535
|
} as Message["metadata"],
|
|
520
536
|
},
|
|
521
537
|
];
|
|
538
|
+
amendmentText = draft.assistantResponse;
|
|
522
539
|
} else {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
},
|
|
530
|
-
];
|
|
540
|
+
pushedAssistant = {
|
|
541
|
+
role: "assistant" as const,
|
|
542
|
+
content: draft.assistantResponse,
|
|
543
|
+
metadata: buildAssistantMetadata(draft),
|
|
544
|
+
};
|
|
545
|
+
conv.messages = [...prevMessages, pushedAssistant];
|
|
531
546
|
}
|
|
532
547
|
}
|
|
533
548
|
applyTurnMetadata(conv, {
|
|
@@ -537,6 +552,62 @@ export class AgentOrchestrator {
|
|
|
537
552
|
harnessMessages: execution?.runHarnessMessages,
|
|
538
553
|
}, { shouldRebuildCanonical: true });
|
|
539
554
|
await this.conversationStore.update(conv);
|
|
555
|
+
|
|
556
|
+
// DUAL-WRITE (mirrors the resume merge/push above). In-place merge →
|
|
557
|
+
// assistant_amendment targeting the latest assistant_message entry
|
|
558
|
+
// (BEST-EFFORT: the blob carries no entry id, so we resolve the target
|
|
559
|
+
// by reading the log's last assistant_message). Fresh bubble →
|
|
560
|
+
// assistant_message. Then parity-check. Fire-and-forget.
|
|
561
|
+
if (amendmentText !== undefined || pushedAssistant) {
|
|
562
|
+
const finalConv = conv;
|
|
563
|
+
const amendText = amendmentText;
|
|
564
|
+
const pushed = pushedAssistant;
|
|
565
|
+
void (async () => {
|
|
566
|
+
try {
|
|
567
|
+
if (pushed) {
|
|
568
|
+
await appendEntriesSafe(
|
|
569
|
+
this.conversationStore,
|
|
570
|
+
finalConv,
|
|
571
|
+
[assistantMessageEntry(pushed, `resume-${conversationId}`, latestRunId)],
|
|
572
|
+
dualWriteLog,
|
|
573
|
+
);
|
|
574
|
+
} else if (amendText !== undefined) {
|
|
575
|
+
const existing = await this.conversationStore.readEntries(
|
|
576
|
+
conversationId,
|
|
577
|
+
{ types: ["assistant_message"] },
|
|
578
|
+
);
|
|
579
|
+
const target = existing[existing.length - 1];
|
|
580
|
+
if (target) {
|
|
581
|
+
await appendEntriesSafe(
|
|
582
|
+
this.conversationStore,
|
|
583
|
+
finalConv,
|
|
584
|
+
[assistantAmendmentEntry(target.id, amendText)],
|
|
585
|
+
dualWriteLog,
|
|
586
|
+
);
|
|
587
|
+
} else {
|
|
588
|
+
dualWriteLog.warn(
|
|
589
|
+
`[entries-dual-write] resume amendment for ${conversationId}: no assistant_message entry to target; skipped`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
await verifyEntriesParity(
|
|
594
|
+
this.conversationStore,
|
|
595
|
+
conversationId,
|
|
596
|
+
{
|
|
597
|
+
harnessMessages: finalConv._harnessMessages,
|
|
598
|
+
displayMessages: finalConv.messages,
|
|
599
|
+
},
|
|
600
|
+
dualWriteLog,
|
|
601
|
+
);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
dualWriteLog.error(
|
|
604
|
+
`[entries-dual-write] resume finalize append failed for ${conversationId}: ${
|
|
605
|
+
err instanceof Error ? err.message : String(err)
|
|
606
|
+
}`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
})();
|
|
610
|
+
}
|
|
540
611
|
}
|
|
541
612
|
} else {
|
|
542
613
|
const conv = await this.conversationStore.get(conversationId);
|
|
@@ -1158,6 +1229,9 @@ export class AgentOrchestrator {
|
|
|
1158
1229
|
const callbackCount = (conversation.subagentCallbackCount ?? 0) + 1;
|
|
1159
1230
|
conversation.subagentCallbackCount = callbackCount;
|
|
1160
1231
|
|
|
1232
|
+
// Collect the injected callback messages so the dual-write can append them
|
|
1233
|
+
// as hidden user_message entries (mirroring the blob pushes below).
|
|
1234
|
+
const injectedCallbackMessages: Message[] = [];
|
|
1161
1235
|
for (const pr of pendingResults) {
|
|
1162
1236
|
// An empty response is recoverable, not a dead end: the subagent's work
|
|
1163
1237
|
// lives in its transcript even when it produced no closing summary (e.g.
|
|
@@ -1172,11 +1246,13 @@ export class AgentOrchestrator {
|
|
|
1172
1246
|
: pr.error
|
|
1173
1247
|
? `Error: ${pr.error.message}`
|
|
1174
1248
|
: "(no result)";
|
|
1175
|
-
|
|
1249
|
+
const injected: Message = {
|
|
1176
1250
|
role: "user",
|
|
1177
1251
|
content: `[Subagent Result] Subagent "${pr.task}" (${pr.subagentId}) ${pr.status}:\n\n${resultBody}`,
|
|
1178
1252
|
metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, timestamp: pr.timestamp } as Message["metadata"],
|
|
1179
|
-
}
|
|
1253
|
+
};
|
|
1254
|
+
injectedCallbackMessages.push(injected);
|
|
1255
|
+
conversation.messages.push(injected);
|
|
1180
1256
|
}
|
|
1181
1257
|
const processedIds = new Set(pendingResults.map(pr => pr.subagentId));
|
|
1182
1258
|
const freshForPending = await this.conversationStore.get(conversationId);
|
|
@@ -1187,6 +1263,48 @@ export class AgentOrchestrator {
|
|
|
1187
1263
|
conversation.updatedAt = Date.now();
|
|
1188
1264
|
await this.conversationStore.update(conversation);
|
|
1189
1265
|
|
|
1266
|
+
// DUAL-WRITE (mirrors the consume-pending + message-push blob writes
|
|
1267
|
+
// above): append a callback_started entry listing the consumed
|
|
1268
|
+
// subagent_result seqs (resolved by matching subagentId against the entry
|
|
1269
|
+
// log — BEST-EFFORT, since the blob's pending array carries no seq), plus
|
|
1270
|
+
// a hidden user_message entry per injected callback message.
|
|
1271
|
+
if (pendingResults.length > 0) {
|
|
1272
|
+
const turnId = `callback-${callbackCount}-${conversation.conversationId}`;
|
|
1273
|
+
void (async () => {
|
|
1274
|
+
try {
|
|
1275
|
+
const resultEntries = await this.conversationStore.readEntries(
|
|
1276
|
+
conversation.conversationId,
|
|
1277
|
+
{ types: ["subagent_result"] },
|
|
1278
|
+
);
|
|
1279
|
+
const consumedIds = new Set(pendingResults.map((pr) => pr.subagentId));
|
|
1280
|
+
const consumedSeqs = resultEntries
|
|
1281
|
+
.filter(
|
|
1282
|
+
(e) =>
|
|
1283
|
+
e.type === "subagent_result" &&
|
|
1284
|
+
consumedIds.has(e.result.subagentId),
|
|
1285
|
+
)
|
|
1286
|
+
.map((e) => e.seq);
|
|
1287
|
+
await appendEntriesSafe(
|
|
1288
|
+
this.conversationStore,
|
|
1289
|
+
conversation,
|
|
1290
|
+
[
|
|
1291
|
+
callbackStartedEntry(consumedSeqs),
|
|
1292
|
+
...injectedCallbackMessages.map((m) =>
|
|
1293
|
+
userMessageEntry(m, turnId, { hidden: true }),
|
|
1294
|
+
),
|
|
1295
|
+
],
|
|
1296
|
+
dualWriteLog,
|
|
1297
|
+
);
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
dualWriteLog.error(
|
|
1300
|
+
`[entries-dual-write] callback_started append failed for ${conversation.conversationId}: ${
|
|
1301
|
+
err instanceof Error ? err.message : String(err)
|
|
1302
|
+
}`,
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
})();
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1190
1308
|
if (callbackCount > MAX_SUBAGENT_CALLBACK_COUNT) {
|
|
1191
1309
|
console.warn(`[poncho][subagent-callback] Circuit breaker: ${callbackCount} callbacks for ${conversationId}, skipping re-run`);
|
|
1192
1310
|
conversation.runningCallbackSince = undefined;
|
|
@@ -1256,12 +1374,14 @@ export class AgentOrchestrator {
|
|
|
1256
1374
|
if (callbackNeedsContinuation || execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0) {
|
|
1257
1375
|
const freshConv = await this.conversationStore.get(conversationId);
|
|
1258
1376
|
if (freshConv) {
|
|
1377
|
+
let callbackAssistantMsg: Message | undefined;
|
|
1259
1378
|
if (!callbackNeedsContinuation) {
|
|
1260
|
-
|
|
1379
|
+
callbackAssistantMsg = {
|
|
1261
1380
|
role: "assistant",
|
|
1262
1381
|
content: execution.draft.assistantResponse,
|
|
1263
1382
|
metadata: buildAssistantMetadata(execution.draft),
|
|
1264
|
-
}
|
|
1383
|
+
};
|
|
1384
|
+
freshConv.messages.push(callbackAssistantMsg);
|
|
1265
1385
|
}
|
|
1266
1386
|
applyTurnMetadata(freshConv, {
|
|
1267
1387
|
latestRunId: execution.latestRunId,
|
|
@@ -1275,6 +1395,30 @@ export class AgentOrchestrator {
|
|
|
1275
1395
|
freshConv.runningCallbackSince = undefined;
|
|
1276
1396
|
await this.conversationStore.update(freshConv);
|
|
1277
1397
|
|
|
1398
|
+
// DUAL-WRITE (mirrors the assistant push above): the callback re-run's
|
|
1399
|
+
// final assistant bubble. Only when not continuing (a continuation has
|
|
1400
|
+
// no final bubble yet). Then run the parity check on the rebuilt
|
|
1401
|
+
// transcript. Fire-and-forget; never blocks.
|
|
1402
|
+
if (callbackAssistantMsg) {
|
|
1403
|
+
const finalMsg = callbackAssistantMsg;
|
|
1404
|
+
void appendEntriesSafe(
|
|
1405
|
+
this.conversationStore,
|
|
1406
|
+
freshConv,
|
|
1407
|
+
[assistantMessageEntry(finalMsg, `callback-${conversationId}`, execution.latestRunId)],
|
|
1408
|
+
dualWriteLog,
|
|
1409
|
+
).then(() =>
|
|
1410
|
+
verifyEntriesParity(
|
|
1411
|
+
this.conversationStore,
|
|
1412
|
+
conversationId,
|
|
1413
|
+
{
|
|
1414
|
+
harnessMessages: freshConv._harnessMessages,
|
|
1415
|
+
displayMessages: freshConv.messages,
|
|
1416
|
+
},
|
|
1417
|
+
dualWriteLog,
|
|
1418
|
+
),
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1278
1422
|
// Proactive messaging notification
|
|
1279
1423
|
if (freshConv.channelMeta && execution.draft.assistantResponse.length > 0) {
|
|
1280
1424
|
this.hooks?.onMessagingNotify?.(conversationId, execution.draft.assistantResponse);
|
|
@@ -1732,6 +1876,28 @@ export class AgentOrchestrator {
|
|
|
1732
1876
|
parentConversationId: string,
|
|
1733
1877
|
result: PendingSubagentResult,
|
|
1734
1878
|
): Promise<boolean> {
|
|
1879
|
+
// DUAL-WRITE (mirrors the appendSubagentResult blob write below): append a
|
|
1880
|
+
// subagent_result entry. Fire-and-forget; needs the parent's owner/tenant,
|
|
1881
|
+
// fetched cheaply. Never blocks or fails the reliable append.
|
|
1882
|
+
void (async () => {
|
|
1883
|
+
try {
|
|
1884
|
+
const parent = await this.conversationStore.get(parentConversationId);
|
|
1885
|
+
if (!parent) return;
|
|
1886
|
+
await appendEntriesSafe(
|
|
1887
|
+
this.conversationStore,
|
|
1888
|
+
parent,
|
|
1889
|
+
[subagentResultEntry(result)],
|
|
1890
|
+
dualWriteLog,
|
|
1891
|
+
);
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
dualWriteLog.error(
|
|
1894
|
+
`[entries-dual-write] subagent_result append failed for ${parentConversationId}: ${
|
|
1895
|
+
err instanceof Error ? err.message : String(err)
|
|
1896
|
+
}`,
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
})();
|
|
1900
|
+
|
|
1735
1901
|
try {
|
|
1736
1902
|
await this.conversationStore.appendSubagentResult(parentConversationId, result);
|
|
1737
1903
|
return true;
|
|
@@ -36,6 +36,15 @@ import {
|
|
|
36
36
|
executeConversationTurn,
|
|
37
37
|
flushTurnDraft,
|
|
38
38
|
} from "./turn.js";
|
|
39
|
+
import {
|
|
40
|
+
appendEntriesSafe,
|
|
41
|
+
assistantMessageEntry,
|
|
42
|
+
compactionEntry,
|
|
43
|
+
harnessMessageEntries,
|
|
44
|
+
newHarnessMessagesThisTurn,
|
|
45
|
+
userMessageEntry,
|
|
46
|
+
verifyEntriesParity,
|
|
47
|
+
} from "./entries-dual-write.js";
|
|
39
48
|
|
|
40
49
|
const log = createLogger("orchestrator");
|
|
41
50
|
|
|
@@ -185,6 +194,14 @@ export const runConversationTurn = async (
|
|
|
185
194
|
await opts.conversationStore.update(conversation);
|
|
186
195
|
};
|
|
187
196
|
|
|
197
|
+
// Snapshot the harness-message array as it stood BEFORE this turn so the
|
|
198
|
+
// finalize path can diff out the messages this turn appended (dual-write).
|
|
199
|
+
const preTurnHarnessMessages = conversation._harnessMessages
|
|
200
|
+
? [...conversation._harnessMessages]
|
|
201
|
+
: undefined;
|
|
202
|
+
// The stable per-turn id used to group dual-write entries.
|
|
203
|
+
const turnId = assistantId;
|
|
204
|
+
|
|
188
205
|
// Persist the user turn immediately so a crash mid-run still records what
|
|
189
206
|
// the user said. Fire-and-forget — don't block the run.
|
|
190
207
|
conversation.messages = [...historyMessages, userMessage];
|
|
@@ -197,6 +214,15 @@ export const runConversationTurn = async (
|
|
|
197
214
|
);
|
|
198
215
|
});
|
|
199
216
|
|
|
217
|
+
// DUAL-WRITE (additive, mirrors the user-turn blob write above): append a
|
|
218
|
+
// user_message entry. Fire-and-forget — never blocks or breaks the turn.
|
|
219
|
+
void appendEntriesSafe(
|
|
220
|
+
opts.conversationStore,
|
|
221
|
+
conversation,
|
|
222
|
+
[userMessageEntry(userMessage, turnId)],
|
|
223
|
+
log,
|
|
224
|
+
);
|
|
225
|
+
|
|
200
226
|
try {
|
|
201
227
|
const execution = await executeConversationTurn({
|
|
202
228
|
harness: opts.harness,
|
|
@@ -247,6 +273,48 @@ export const runConversationTurn = async (
|
|
|
247
273
|
...existingHistory,
|
|
248
274
|
...preRunMessages.slice(0, removedCount),
|
|
249
275
|
];
|
|
276
|
+
|
|
277
|
+
// DUAL-WRITE (mirrors the compactedHistory blob write above): the
|
|
278
|
+
// compacted array is [summaryMessage, ...keptMessages]. BEST-EFFORT
|
|
279
|
+
// firstKeptSeq: the entry-log seqs of the kept harness messages
|
|
280
|
+
// aren't known here, so we derive a sentinel from the kept-count by
|
|
281
|
+
// reading the current max harness_message seq and pointing at the
|
|
282
|
+
// tail. We read the existing entries to compute it.
|
|
283
|
+
const summaryMessage = event.compactedMessages[0];
|
|
284
|
+
const keptCount = Math.max(0, event.compactedMessages.length - 1);
|
|
285
|
+
if (summaryMessage) {
|
|
286
|
+
void (async () => {
|
|
287
|
+
try {
|
|
288
|
+
const existing = await opts.conversationStore.readEntries(
|
|
289
|
+
opts.conversationId,
|
|
290
|
+
{ types: ["harness_message"] },
|
|
291
|
+
);
|
|
292
|
+
// firstKeptSeq = seq of the (keptCount)-th-from-last existing
|
|
293
|
+
// harness message, so rebuild keeps exactly that many.
|
|
294
|
+
const harnessSeqs = existing.map((e) => e.seq);
|
|
295
|
+
const firstKeptSeq =
|
|
296
|
+
harnessSeqs.length >= keptCount && keptCount > 0
|
|
297
|
+
? harnessSeqs[harnessSeqs.length - keptCount]!
|
|
298
|
+
: (harnessSeqs[harnessSeqs.length - 1] ?? 0) + 1;
|
|
299
|
+
await appendEntriesSafe(
|
|
300
|
+
opts.conversationStore,
|
|
301
|
+
conversation,
|
|
302
|
+
[
|
|
303
|
+
compactionEntry(summaryMessage, firstKeptSeq, {
|
|
304
|
+
tokensBefore: conversation.contextTokens,
|
|
305
|
+
}),
|
|
306
|
+
],
|
|
307
|
+
log,
|
|
308
|
+
);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
log.error(
|
|
311
|
+
`[entries-dual-write] compaction append failed: ${
|
|
312
|
+
err instanceof Error ? err.message : String(err)
|
|
313
|
+
}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
})();
|
|
317
|
+
}
|
|
250
318
|
}
|
|
251
319
|
}
|
|
252
320
|
if (event.type === "step:completed") {
|
|
@@ -387,6 +455,46 @@ export const runConversationTurn = async (
|
|
|
387
455
|
{ shouldRebuildCanonical },
|
|
388
456
|
);
|
|
389
457
|
await opts.conversationStore.update(conversation);
|
|
458
|
+
|
|
459
|
+
// DUAL-WRITE at finalize (mirrors applyTurnMetadata's _harnessMessages
|
|
460
|
+
// write + the final assistant bubble in conversation.messages):
|
|
461
|
+
// 1. harness_message entries for the messages this turn appended,
|
|
462
|
+
// 2. the final assistant_message entry.
|
|
463
|
+
// Best-effort + fire-and-forget; never blocks the return.
|
|
464
|
+
const finalAssistant =
|
|
465
|
+
conversation.messages[conversation.messages.length - 1];
|
|
466
|
+
const { messages: newHarness, approximate } = newHarnessMessagesThisTurn(
|
|
467
|
+
preTurnHarnessMessages,
|
|
468
|
+
conversation._harnessMessages,
|
|
469
|
+
);
|
|
470
|
+
if (approximate) {
|
|
471
|
+
log.warn(
|
|
472
|
+
`[entries-dual-write] ${opts.conversationId} harness-message diff approximate ` +
|
|
473
|
+
`(blob array shrank this turn — likely compaction); appended full context`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const finalizeEntries = [
|
|
477
|
+
...harnessMessageEntries(newHarness, turnId),
|
|
478
|
+
...(finalAssistant && finalAssistant.role === "assistant"
|
|
479
|
+
? [assistantMessageEntry(finalAssistant, turnId, latestRunId)]
|
|
480
|
+
: []),
|
|
481
|
+
];
|
|
482
|
+
void appendEntriesSafe(
|
|
483
|
+
opts.conversationStore,
|
|
484
|
+
conversation,
|
|
485
|
+
finalizeEntries,
|
|
486
|
+
log,
|
|
487
|
+
).then(() =>
|
|
488
|
+
verifyEntriesParity(
|
|
489
|
+
opts.conversationStore,
|
|
490
|
+
opts.conversationId,
|
|
491
|
+
{
|
|
492
|
+
harnessMessages: conversation._harnessMessages,
|
|
493
|
+
displayMessages: conversation.messages,
|
|
494
|
+
},
|
|
495
|
+
log,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
390
498
|
}
|
|
391
499
|
|
|
392
500
|
return {
|
package/src/state.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from "@poncho-ai/sdk";
|
|
2
|
+
import type { ConversationEntry, NewConversationEntry } from "./storage/entries.js";
|
|
2
3
|
|
|
3
4
|
export interface ConversationState {
|
|
4
5
|
runId: string;
|
|
@@ -142,6 +143,23 @@ export interface ConversationStore {
|
|
|
142
143
|
clearCallbackLock(conversationId: string): Promise<Conversation | undefined>;
|
|
143
144
|
/** List thread conversations anchored under `parentConversationId`. */
|
|
144
145
|
listThreads(parentConversationId: string): Promise<ConversationSummary[]>;
|
|
146
|
+
/**
|
|
147
|
+
* Append entries to a conversation's append-only log (Phase 3 substrate).
|
|
148
|
+
* Assigns a per-conversation monotonic `seq` and a `createdAt` to each
|
|
149
|
+
* entry, persists them in order, and returns the stored entries with those
|
|
150
|
+
* fields filled in. Additive — no existing read path consumes these yet.
|
|
151
|
+
*/
|
|
152
|
+
appendEntries(
|
|
153
|
+
conversationId: string,
|
|
154
|
+
agentId: string,
|
|
155
|
+
tenantId: string | null,
|
|
156
|
+
entries: NewConversationEntry[],
|
|
157
|
+
): Promise<ConversationEntry[]>;
|
|
158
|
+
/** Read a conversation's entries ordered by `seq` ascending. */
|
|
159
|
+
readEntries(
|
|
160
|
+
conversationId: string,
|
|
161
|
+
opts?: { types?: string[]; afterSeq?: number; limit?: number },
|
|
162
|
+
): Promise<ConversationEntry[]>;
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
export type StateProviderName =
|
|
@@ -201,6 +219,7 @@ export class InMemoryStateStore implements StateStore {
|
|
|
201
219
|
|
|
202
220
|
export class InMemoryConversationStore implements ConversationStore {
|
|
203
221
|
private readonly conversations = new Map<string, Conversation>();
|
|
222
|
+
private readonly entries = new Map<string, ConversationEntry[]>();
|
|
204
223
|
private readonly ttlMs?: number;
|
|
205
224
|
|
|
206
225
|
constructor(ttlSeconds?: number) {
|
|
@@ -372,6 +391,43 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
372
391
|
channelMeta: c.channelMeta,
|
|
373
392
|
}));
|
|
374
393
|
}
|
|
394
|
+
|
|
395
|
+
async appendEntries(
|
|
396
|
+
conversationId: string,
|
|
397
|
+
_agentId: string,
|
|
398
|
+
_tenantId: string | null,
|
|
399
|
+
entries: NewConversationEntry[],
|
|
400
|
+
): Promise<ConversationEntry[]> {
|
|
401
|
+
const list = this.entries.get(conversationId) ?? [];
|
|
402
|
+
// seq is per-conversation: max existing seq + 1, then consecutive.
|
|
403
|
+
let nextSeq = list.reduce((max, e) => (e.seq > max ? e.seq : max), 0) + 1;
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const stored: ConversationEntry[] = entries.map(
|
|
406
|
+
(e) => ({ ...e, seq: nextSeq++, createdAt: now }) as ConversationEntry,
|
|
407
|
+
);
|
|
408
|
+
this.entries.set(conversationId, [...list, ...stored]);
|
|
409
|
+
return stored;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async readEntries(
|
|
413
|
+
conversationId: string,
|
|
414
|
+
opts?: { types?: string[]; afterSeq?: number; limit?: number },
|
|
415
|
+
): Promise<ConversationEntry[]> {
|
|
416
|
+
let list = (this.entries.get(conversationId) ?? [])
|
|
417
|
+
.slice()
|
|
418
|
+
.sort((a, b) => a.seq - b.seq);
|
|
419
|
+
if (opts?.types && opts.types.length > 0) {
|
|
420
|
+
const allowed = new Set(opts.types);
|
|
421
|
+
list = list.filter((e) => allowed.has(e.type));
|
|
422
|
+
}
|
|
423
|
+
if (typeof opts?.afterSeq === "number") {
|
|
424
|
+
list = list.filter((e) => e.seq > opts.afterSeq!);
|
|
425
|
+
}
|
|
426
|
+
if (typeof opts?.limit === "number") {
|
|
427
|
+
list = list.slice(0, opts.limit);
|
|
428
|
+
}
|
|
429
|
+
return list;
|
|
430
|
+
}
|
|
375
431
|
}
|
|
376
432
|
|
|
377
433
|
export type ConversationSummary = {
|
package/src/storage/engine.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ConversationSummary,
|
|
6
6
|
PendingSubagentResult,
|
|
7
7
|
} from "../state.js";
|
|
8
|
+
import type { ConversationEntry, NewConversationEntry } from "./entries.js";
|
|
8
9
|
import type { MainMemory } from "../memory.js";
|
|
9
10
|
import type { TodoItem } from "../todo-tools.js";
|
|
10
11
|
import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
|
|
@@ -77,6 +78,23 @@ export interface StorageEngine {
|
|
|
77
78
|
clearCallbackLock(conversationId: string): Promise<Conversation | undefined>;
|
|
78
79
|
/** List thread conversations anchored under `parentConversationId`. */
|
|
79
80
|
listThreads(parentConversationId: string): Promise<ConversationSummary[]>;
|
|
81
|
+
/**
|
|
82
|
+
* Append entries to a conversation's append-only log (Phase 3 substrate).
|
|
83
|
+
* Assigns a per-conversation monotonic `seq` and `createdAt` to each entry,
|
|
84
|
+
* persists them in order, and returns the stored entries. Additive — no
|
|
85
|
+
* read path consumes these yet.
|
|
86
|
+
*/
|
|
87
|
+
appendEntries(
|
|
88
|
+
conversationId: string,
|
|
89
|
+
agentId: string,
|
|
90
|
+
tenantId: string | null,
|
|
91
|
+
entries: NewConversationEntry[],
|
|
92
|
+
): Promise<ConversationEntry[]>;
|
|
93
|
+
/** Read a conversation's entries ordered by `seq` ascending. */
|
|
94
|
+
readEntries(
|
|
95
|
+
conversationId: string,
|
|
96
|
+
opts?: { types?: string[]; afterSeq?: number; limit?: number },
|
|
97
|
+
): Promise<ConversationEntry[]>;
|
|
80
98
|
};
|
|
81
99
|
|
|
82
100
|
// --- Memory (replaces MemoryStore) ---
|