@poncho-ai/harness 0.22.0 → 0.22.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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +79 -25
- package/package.json +1 -1
- package/src/harness.ts +29 -6
- package/src/memory.ts +4 -4
- package/src/state.ts +59 -23
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.22.
|
|
2
|
+
> @poncho-ai/harness@0.22.1 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[embed-docs] Generated poncho-docs.ts with 4 topics
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
12
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m260.03 KB[39m
|
|
12
|
+
[32mESM[39m ⚡️ Build success in 131ms
|
|
13
13
|
[34mDTS[39m Build start
|
|
14
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m27.
|
|
14
|
+
[32mDTS[39m ⚡️ Build success in 6723ms
|
|
15
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m27.15 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.22.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`096953d`](https://github.com/cesr/poncho-ai/commit/096953d5a64a785950ea0a7f09e2183e481afd29) Thanks [@cesr](https://github.com/cesr)! - Improve time-to-first-token by lazy-loading the recall corpus
|
|
8
|
+
|
|
9
|
+
The recall corpus (past conversation summaries) is now fetched on-demand only when the LLM invokes the `conversation_recall` tool, instead of blocking every message with ~1.3s of upfront I/O. Also adds batch `mget` support to Upstash/Redis/DynamoDB conversation stores, parallelizes memory fetch with skill refresh, debounces skill refresh in dev mode, and caches message conversions across multi-step runs.
|
|
10
|
+
|
|
3
11
|
## 0.22.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -562,6 +562,7 @@ declare class AgentHarness {
|
|
|
562
562
|
private loadedConfig?;
|
|
563
563
|
private loadedSkills;
|
|
564
564
|
private skillFingerprint;
|
|
565
|
+
private lastSkillRefreshAt;
|
|
565
566
|
private readonly activeSkillNames;
|
|
566
567
|
private readonly registeredMcpToolNames;
|
|
567
568
|
private latitudeTelemetry?;
|
|
@@ -601,6 +602,7 @@ declare class AgentHarness {
|
|
|
601
602
|
private refreshMcpTools;
|
|
602
603
|
private buildSkillFingerprint;
|
|
603
604
|
private registerSkillTools;
|
|
605
|
+
private static readonly SKILL_REFRESH_DEBOUNCE_MS;
|
|
604
606
|
private refreshSkillsIfChanged;
|
|
605
607
|
initialize(): Promise<void>;
|
|
606
608
|
private buildBrowserStoragePersistence;
|
package/dist/index.js
CHANGED
|
@@ -2771,10 +2771,9 @@ var createMemoryTools = (store, options) => {
|
|
|
2771
2771
|
Math.min(5, typeof input.limit === "number" ? input.limit : 3)
|
|
2772
2772
|
);
|
|
2773
2773
|
const excludeConversationId = typeof input.excludeConversationId === "string" ? input.excludeConversationId : "";
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
);
|
|
2774
|
+
const rawCorpus = context.parameters.__conversationRecallCorpus;
|
|
2775
|
+
const resolvedCorpus = typeof rawCorpus === "function" ? await rawCorpus() : rawCorpus;
|
|
2776
|
+
const corpus = asRecallCorpus(resolvedCorpus).slice(0, maxRecallConversations);
|
|
2778
2777
|
const results = corpus.filter(
|
|
2779
2778
|
(item) => excludeConversationId ? item.conversationId !== excludeConversationId : true
|
|
2780
2779
|
).map((item) => ({
|
|
@@ -4599,7 +4598,7 @@ function extractMediaFromToolOutput(output) {
|
|
|
4599
4598
|
const strippedOutput = walk(output);
|
|
4600
4599
|
return { mediaItems, strippedOutput };
|
|
4601
4600
|
}
|
|
4602
|
-
var AgentHarness = class {
|
|
4601
|
+
var AgentHarness = class _AgentHarness {
|
|
4603
4602
|
workingDir;
|
|
4604
4603
|
environment;
|
|
4605
4604
|
modelProvider;
|
|
@@ -4611,6 +4610,7 @@ var AgentHarness = class {
|
|
|
4611
4610
|
loadedConfig;
|
|
4612
4611
|
loadedSkills = [];
|
|
4613
4612
|
skillFingerprint = "";
|
|
4613
|
+
lastSkillRefreshAt = 0;
|
|
4614
4614
|
activeSkillNames = /* @__PURE__ */ new Set();
|
|
4615
4615
|
registeredMcpToolNames = /* @__PURE__ */ new Set();
|
|
4616
4616
|
latitudeTelemetry;
|
|
@@ -4899,10 +4899,16 @@ var AgentHarness = class {
|
|
|
4899
4899
|
})
|
|
4900
4900
|
);
|
|
4901
4901
|
}
|
|
4902
|
+
static SKILL_REFRESH_DEBOUNCE_MS = 3e3;
|
|
4902
4903
|
async refreshSkillsIfChanged() {
|
|
4903
4904
|
if (this.environment !== "development") {
|
|
4904
4905
|
return;
|
|
4905
4906
|
}
|
|
4907
|
+
const elapsed = Date.now() - this.lastSkillRefreshAt;
|
|
4908
|
+
if (this.lastSkillRefreshAt > 0 && elapsed < _AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
this.lastSkillRefreshAt = Date.now();
|
|
4906
4912
|
try {
|
|
4907
4913
|
const latestSkills = await loadSkillMetadata(
|
|
4908
4914
|
this.workingDir,
|
|
@@ -5228,6 +5234,7 @@ var AgentHarness = class {
|
|
|
5228
5234
|
if (!this.parsedAgent) {
|
|
5229
5235
|
await this.initialize();
|
|
5230
5236
|
}
|
|
5237
|
+
const memoryPromise = this.memoryStore ? this.memoryStore.getMainMemory() : void 0;
|
|
5231
5238
|
await this.refreshSkillsIfChanged();
|
|
5232
5239
|
this._currentRunConversationId = input.conversationId;
|
|
5233
5240
|
const ownerParam = input.parameters?.__ownerId;
|
|
@@ -5280,7 +5287,7 @@ Each conversation gets its own browser tab sharing a single browser instance. Ca
|
|
|
5280
5287
|
const promptWithSkills = this.skillContextWindow ? `${systemPrompt}${developmentContext}
|
|
5281
5288
|
|
|
5282
5289
|
${this.skillContextWindow}${browserContext}` : `${systemPrompt}${developmentContext}${browserContext}`;
|
|
5283
|
-
const mainMemory =
|
|
5290
|
+
const mainMemory = await memoryPromise;
|
|
5284
5291
|
const boundedMainMemory = mainMemory && mainMemory.content.length > 4e3 ? `${mainMemory.content.slice(0, 4e3)}
|
|
5285
5292
|
...[truncated]` : mainMemory?.content;
|
|
5286
5293
|
const memoryContext = boundedMainMemory && boundedMainMemory.trim().length > 0 ? `
|
|
@@ -5375,6 +5382,8 @@ ${boundedMainMemory.trim()}` : "";
|
|
|
5375
5382
|
let totalOutputTokens = 0;
|
|
5376
5383
|
let totalCachedTokens = 0;
|
|
5377
5384
|
let transientStepRetryCount = 0;
|
|
5385
|
+
let cachedCoreMessages = [];
|
|
5386
|
+
let convertedUpTo = 0;
|
|
5378
5387
|
for (let step = 1; step <= maxSteps; step += 1) {
|
|
5379
5388
|
try {
|
|
5380
5389
|
yield* drainBrowserEvents();
|
|
@@ -5651,7 +5660,15 @@ ${textContent}` };
|
|
|
5651
5660
|
}
|
|
5652
5661
|
}
|
|
5653
5662
|
}
|
|
5654
|
-
|
|
5663
|
+
if (convertedUpTo > messages.length) {
|
|
5664
|
+
cachedCoreMessages = [];
|
|
5665
|
+
convertedUpTo = 0;
|
|
5666
|
+
}
|
|
5667
|
+
const newMessages = messages.slice(convertedUpTo);
|
|
5668
|
+
const newCoreMessages = newMessages.length > 0 ? (await Promise.all(newMessages.map(convertMessage))).flat() : [];
|
|
5669
|
+
cachedCoreMessages = [...cachedCoreMessages, ...newCoreMessages];
|
|
5670
|
+
convertedUpTo = messages.length;
|
|
5671
|
+
const coreMessages = cachedCoreMessages;
|
|
5655
5672
|
const temperature = agent.frontmatter.model?.temperature ?? 0.2;
|
|
5656
5673
|
const maxTokens = agent.frontmatter.model?.maxTokens;
|
|
5657
5674
|
const cachedMessages = addPromptCacheBreakpoints(coreMessages, modelInstance);
|
|
@@ -6750,12 +6767,12 @@ var KeyValueConversationStoreBase = class {
|
|
|
6750
6767
|
return [];
|
|
6751
6768
|
}
|
|
6752
6769
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
6770
|
+
if (ids.length === 0) return [];
|
|
6771
|
+
const convKeys = await Promise.all(ids.map((id) => this.conversationKey(id)));
|
|
6772
|
+
const rawValues = await kv.mget(convKeys);
|
|
6753
6773
|
const conversations = [];
|
|
6754
|
-
for (const
|
|
6755
|
-
|
|
6756
|
-
if (!raw) {
|
|
6757
|
-
continue;
|
|
6758
|
-
}
|
|
6774
|
+
for (const raw of rawValues) {
|
|
6775
|
+
if (!raw) continue;
|
|
6759
6776
|
try {
|
|
6760
6777
|
conversations.push(JSON.parse(raw));
|
|
6761
6778
|
} catch {
|
|
@@ -6772,20 +6789,27 @@ var KeyValueConversationStoreBase = class {
|
|
|
6772
6789
|
return [];
|
|
6773
6790
|
}
|
|
6774
6791
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
6792
|
+
if (ids.length === 0) return [];
|
|
6793
|
+
const metaKeys = await Promise.all(ids.map((id) => this.conversationMetaKey(id)));
|
|
6794
|
+
const rawValues = await kv.mget(metaKeys);
|
|
6775
6795
|
const summaries = [];
|
|
6776
|
-
for (const
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6796
|
+
for (const raw of rawValues) {
|
|
6797
|
+
if (!raw) continue;
|
|
6798
|
+
try {
|
|
6799
|
+
const meta = JSON.parse(raw);
|
|
6800
|
+
if (meta.ownerId === ownerId) {
|
|
6801
|
+
summaries.push({
|
|
6802
|
+
conversationId: meta.conversationId,
|
|
6803
|
+
title: meta.title,
|
|
6804
|
+
updatedAt: meta.updatedAt,
|
|
6805
|
+
createdAt: meta.createdAt,
|
|
6806
|
+
ownerId: meta.ownerId,
|
|
6807
|
+
parentConversationId: meta.parentConversationId,
|
|
6808
|
+
messageCount: meta.messageCount,
|
|
6809
|
+
hasPendingApprovals: meta.hasPendingApprovals
|
|
6810
|
+
});
|
|
6811
|
+
}
|
|
6812
|
+
} catch {
|
|
6789
6813
|
}
|
|
6790
6814
|
}
|
|
6791
6815
|
return summaries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
@@ -6923,6 +6947,19 @@ var UpstashConversationStore = class extends KeyValueConversationStoreBase {
|
|
|
6923
6947
|
const payload = await response.json();
|
|
6924
6948
|
return payload.result ?? void 0;
|
|
6925
6949
|
},
|
|
6950
|
+
mget: async (keys) => {
|
|
6951
|
+
if (keys.length === 0) return [];
|
|
6952
|
+
const path = keys.map((k) => encodeURIComponent(k)).join("/");
|
|
6953
|
+
const response = await fetch(`${this.baseUrl}/mget/${path}`, {
|
|
6954
|
+
method: "POST",
|
|
6955
|
+
headers: this.headers()
|
|
6956
|
+
});
|
|
6957
|
+
if (!response.ok) {
|
|
6958
|
+
return keys.map(() => void 0);
|
|
6959
|
+
}
|
|
6960
|
+
const payload = await response.json();
|
|
6961
|
+
return (payload.result ?? []).map((v) => v ?? void 0);
|
|
6962
|
+
},
|
|
6926
6963
|
set: async (key, value, ttl) => {
|
|
6927
6964
|
const endpoint = typeof ttl === "number" ? `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(
|
|
6928
6965
|
1,
|
|
@@ -7015,6 +7052,11 @@ var RedisLikeConversationStore = class extends KeyValueConversationStoreBase {
|
|
|
7015
7052
|
const value = await client.get(key);
|
|
7016
7053
|
return value ?? void 0;
|
|
7017
7054
|
},
|
|
7055
|
+
mget: async (keys) => {
|
|
7056
|
+
if (keys.length === 0) return [];
|
|
7057
|
+
const values = await client.mGet(keys);
|
|
7058
|
+
return values.map((v) => v ?? void 0);
|
|
7059
|
+
},
|
|
7018
7060
|
set: async (key, value, ttl) => {
|
|
7019
7061
|
if (typeof ttl === "number") {
|
|
7020
7062
|
await client.set(key, value, { EX: Math.max(1, ttl) });
|
|
@@ -7149,6 +7191,18 @@ var DynamoDbConversationStore = class extends KeyValueConversationStoreBase {
|
|
|
7149
7191
|
})
|
|
7150
7192
|
);
|
|
7151
7193
|
},
|
|
7194
|
+
mget: async (keys) => {
|
|
7195
|
+
if (keys.length === 0) return [];
|
|
7196
|
+
return Promise.all(keys.map(async (key) => {
|
|
7197
|
+
const result = await client.send(
|
|
7198
|
+
new client.GetItemCommand({
|
|
7199
|
+
TableName: this.table,
|
|
7200
|
+
Key: { runId: { S: key } }
|
|
7201
|
+
})
|
|
7202
|
+
);
|
|
7203
|
+
return result.Item?.value?.S;
|
|
7204
|
+
}));
|
|
7205
|
+
},
|
|
7152
7206
|
del: async (key) => {
|
|
7153
7207
|
await client.send(
|
|
7154
7208
|
new client.DeleteItemCommand({
|
package/package.json
CHANGED
package/src/harness.ts
CHANGED
|
@@ -545,6 +545,7 @@ export class AgentHarness {
|
|
|
545
545
|
private loadedConfig?: PonchoConfig;
|
|
546
546
|
private loadedSkills: SkillMetadata[] = [];
|
|
547
547
|
private skillFingerprint = "";
|
|
548
|
+
private lastSkillRefreshAt = 0;
|
|
548
549
|
private readonly activeSkillNames = new Set<string>();
|
|
549
550
|
private readonly registeredMcpToolNames = new Set<string>();
|
|
550
551
|
private latitudeTelemetry?: LatitudeTelemetry;
|
|
@@ -879,10 +880,17 @@ export class AgentHarness {
|
|
|
879
880
|
);
|
|
880
881
|
}
|
|
881
882
|
|
|
883
|
+
private static readonly SKILL_REFRESH_DEBOUNCE_MS = 3000;
|
|
884
|
+
|
|
882
885
|
private async refreshSkillsIfChanged(): Promise<void> {
|
|
883
886
|
if (this.environment !== "development") {
|
|
884
887
|
return;
|
|
885
888
|
}
|
|
889
|
+
const elapsed = Date.now() - this.lastSkillRefreshAt;
|
|
890
|
+
if (this.lastSkillRefreshAt > 0 && elapsed < AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
this.lastSkillRefreshAt = Date.now();
|
|
886
894
|
try {
|
|
887
895
|
const latestSkills = await loadSkillMetadata(
|
|
888
896
|
this.workingDir,
|
|
@@ -1269,6 +1277,10 @@ export class AgentHarness {
|
|
|
1269
1277
|
if (!this.parsedAgent) {
|
|
1270
1278
|
await this.initialize();
|
|
1271
1279
|
}
|
|
1280
|
+
// Start memory fetch early so it overlaps with skill refresh I/O
|
|
1281
|
+
const memoryPromise = this.memoryStore
|
|
1282
|
+
? this.memoryStore.getMainMemory()
|
|
1283
|
+
: undefined;
|
|
1272
1284
|
await this.refreshSkillsIfChanged();
|
|
1273
1285
|
|
|
1274
1286
|
// Track which conversation/owner this run belongs to so browser & subagent tools resolve correctly
|
|
@@ -1328,9 +1340,7 @@ Each conversation gets its own browser tab sharing a single browser instance. Ca
|
|
|
1328
1340
|
const promptWithSkills = this.skillContextWindow
|
|
1329
1341
|
? `${systemPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
|
|
1330
1342
|
: `${systemPrompt}${developmentContext}${browserContext}`;
|
|
1331
|
-
const mainMemory =
|
|
1332
|
-
? await this.memoryStore.getMainMemory()
|
|
1333
|
-
: undefined;
|
|
1343
|
+
const mainMemory = await memoryPromise;
|
|
1334
1344
|
const boundedMainMemory =
|
|
1335
1345
|
mainMemory && mainMemory.content.length > 4000
|
|
1336
1346
|
? `${mainMemory.content.slice(0, 4000)}\n...[truncated]`
|
|
@@ -1445,6 +1455,8 @@ ${boundedMainMemory.trim()}`
|
|
|
1445
1455
|
let totalOutputTokens = 0;
|
|
1446
1456
|
let totalCachedTokens = 0;
|
|
1447
1457
|
let transientStepRetryCount = 0;
|
|
1458
|
+
let cachedCoreMessages: ModelMessage[] = [];
|
|
1459
|
+
let convertedUpTo = 0;
|
|
1448
1460
|
|
|
1449
1461
|
for (let step = 1; step <= maxSteps; step += 1) {
|
|
1450
1462
|
try {
|
|
@@ -1784,9 +1796,19 @@ ${boundedMainMemory.trim()}`
|
|
|
1784
1796
|
}
|
|
1785
1797
|
}
|
|
1786
1798
|
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1799
|
+
// Only convert messages added since the last step
|
|
1800
|
+
if (convertedUpTo > messages.length) {
|
|
1801
|
+
// Compaction replaced the array — invalidate cache
|
|
1802
|
+
cachedCoreMessages = [];
|
|
1803
|
+
convertedUpTo = 0;
|
|
1804
|
+
}
|
|
1805
|
+
const newMessages = messages.slice(convertedUpTo);
|
|
1806
|
+
const newCoreMessages: ModelMessage[] = newMessages.length > 0
|
|
1807
|
+
? (await Promise.all(newMessages.map(convertMessage))).flat()
|
|
1808
|
+
: [];
|
|
1809
|
+
cachedCoreMessages = [...cachedCoreMessages, ...newCoreMessages];
|
|
1810
|
+
convertedUpTo = messages.length;
|
|
1811
|
+
const coreMessages = cachedCoreMessages;
|
|
1790
1812
|
|
|
1791
1813
|
const temperature = agent.frontmatter.model?.temperature ?? 0.2;
|
|
1792
1814
|
const maxTokens = agent.frontmatter.model?.maxTokens;
|
|
@@ -1794,6 +1816,7 @@ ${boundedMainMemory.trim()}`
|
|
|
1794
1816
|
|
|
1795
1817
|
const telemetryEnabled = this.loadedConfig?.telemetry?.enabled !== false;
|
|
1796
1818
|
|
|
1819
|
+
|
|
1797
1820
|
const result = await streamText({
|
|
1798
1821
|
model: modelInstance,
|
|
1799
1822
|
system: integrityPrompt,
|
package/src/memory.ts
CHANGED
|
@@ -658,10 +658,10 @@ export const createMemoryTools = (
|
|
|
658
658
|
typeof input.excludeConversationId === "string"
|
|
659
659
|
? input.excludeConversationId
|
|
660
660
|
: "";
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
);
|
|
661
|
+
const rawCorpus = context.parameters.__conversationRecallCorpus;
|
|
662
|
+
const resolvedCorpus =
|
|
663
|
+
typeof rawCorpus === "function" ? await (rawCorpus as () => Promise<unknown>)() : rawCorpus;
|
|
664
|
+
const corpus = asRecallCorpus(resolvedCorpus).slice(0, maxRecallConversations);
|
|
665
665
|
const results = corpus
|
|
666
666
|
.filter((item) =>
|
|
667
667
|
excludeConversationId ? item.conversationId !== excludeConversationId : true,
|
package/src/state.ts
CHANGED
|
@@ -646,6 +646,7 @@ class FileStateStore implements StateStore {
|
|
|
646
646
|
|
|
647
647
|
interface RawKeyValueClient {
|
|
648
648
|
get(key: string): Promise<string | undefined>;
|
|
649
|
+
mget(keys: string[]): Promise<(string | undefined)[]>;
|
|
649
650
|
set(key: string, value: string, ttl?: number): Promise<void>;
|
|
650
651
|
del(key: string): Promise<void>;
|
|
651
652
|
}
|
|
@@ -760,21 +761,18 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
760
761
|
return await this.memoryFallback.list(ownerId);
|
|
761
762
|
}
|
|
762
763
|
if (!ownerId) {
|
|
763
|
-
// KV stores index per-owner; cross-owner listing not supported
|
|
764
764
|
return [];
|
|
765
765
|
}
|
|
766
766
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
767
|
+
if (ids.length === 0) return [];
|
|
768
|
+
const convKeys = await Promise.all(ids.map((id) => this.conversationKey(id)));
|
|
769
|
+
const rawValues = await kv.mget(convKeys);
|
|
767
770
|
const conversations: Conversation[] = [];
|
|
768
|
-
for (const
|
|
769
|
-
|
|
770
|
-
if (!raw) {
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
771
|
+
for (const raw of rawValues) {
|
|
772
|
+
if (!raw) continue;
|
|
773
773
|
try {
|
|
774
774
|
conversations.push(JSON.parse(raw) as Conversation);
|
|
775
|
-
} catch {
|
|
776
|
-
// Skip invalid records.
|
|
777
|
-
}
|
|
775
|
+
} catch { /* skip invalid records */ }
|
|
778
776
|
}
|
|
779
777
|
return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
780
778
|
}
|
|
@@ -788,21 +786,27 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
788
786
|
return [];
|
|
789
787
|
}
|
|
790
788
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
789
|
+
if (ids.length === 0) return [];
|
|
790
|
+
const metaKeys = await Promise.all(ids.map((id) => this.conversationMetaKey(id)));
|
|
791
|
+
const rawValues = await kv.mget(metaKeys);
|
|
791
792
|
const summaries: ConversationSummary[] = [];
|
|
792
|
-
for (const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
793
|
+
for (const raw of rawValues) {
|
|
794
|
+
if (!raw) continue;
|
|
795
|
+
try {
|
|
796
|
+
const meta = JSON.parse(raw) as ConversationMeta;
|
|
797
|
+
if (meta.ownerId === ownerId) {
|
|
798
|
+
summaries.push({
|
|
799
|
+
conversationId: meta.conversationId,
|
|
800
|
+
title: meta.title,
|
|
801
|
+
updatedAt: meta.updatedAt,
|
|
802
|
+
createdAt: meta.createdAt,
|
|
803
|
+
ownerId: meta.ownerId,
|
|
804
|
+
parentConversationId: meta.parentConversationId,
|
|
805
|
+
messageCount: meta.messageCount,
|
|
806
|
+
hasPendingApprovals: meta.hasPendingApprovals,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
} catch { /* skip invalid records */ }
|
|
806
810
|
}
|
|
807
811
|
return summaries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
808
812
|
}
|
|
@@ -948,6 +952,19 @@ class UpstashConversationStore extends KeyValueConversationStoreBase {
|
|
|
948
952
|
const payload = (await response.json()) as { result?: string | null };
|
|
949
953
|
return payload.result ?? undefined;
|
|
950
954
|
},
|
|
955
|
+
mget: async (keys: string[]) => {
|
|
956
|
+
if (keys.length === 0) return [];
|
|
957
|
+
const path = keys.map((k) => encodeURIComponent(k)).join("/");
|
|
958
|
+
const response = await fetch(`${this.baseUrl}/mget/${path}`, {
|
|
959
|
+
method: "POST",
|
|
960
|
+
headers: this.headers(),
|
|
961
|
+
});
|
|
962
|
+
if (!response.ok) {
|
|
963
|
+
return keys.map(() => undefined);
|
|
964
|
+
}
|
|
965
|
+
const payload = (await response.json()) as { result?: (string | null)[] };
|
|
966
|
+
return (payload.result ?? []).map((v) => v ?? undefined);
|
|
967
|
+
},
|
|
951
968
|
set: async (key: string, value: string, ttl?: number) => {
|
|
952
969
|
const endpoint =
|
|
953
970
|
typeof ttl === "number"
|
|
@@ -1042,6 +1059,7 @@ class RedisLikeConversationStore extends KeyValueConversationStoreBase {
|
|
|
1042
1059
|
private readonly clientPromise: Promise<
|
|
1043
1060
|
| {
|
|
1044
1061
|
get: (key: string) => Promise<string | null>;
|
|
1062
|
+
mGet: (keys: readonly string[]) => Promise<(string | null)[]>;
|
|
1045
1063
|
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
1046
1064
|
del: (key: string) => Promise<unknown>;
|
|
1047
1065
|
}
|
|
@@ -1056,6 +1074,7 @@ class RedisLikeConversationStore extends KeyValueConversationStoreBase {
|
|
|
1056
1074
|
createClient: (options: { url: string }) => {
|
|
1057
1075
|
connect: () => Promise<unknown>;
|
|
1058
1076
|
get: (key: string) => Promise<string | null>;
|
|
1077
|
+
mGet: (keys: readonly string[]) => Promise<(string | null)[]>;
|
|
1059
1078
|
set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
|
|
1060
1079
|
del: (key: string) => Promise<unknown>;
|
|
1061
1080
|
};
|
|
@@ -1079,6 +1098,11 @@ class RedisLikeConversationStore extends KeyValueConversationStoreBase {
|
|
|
1079
1098
|
const value = await client.get(key);
|
|
1080
1099
|
return value ?? undefined;
|
|
1081
1100
|
},
|
|
1101
|
+
mget: async (keys: string[]) => {
|
|
1102
|
+
if (keys.length === 0) return [];
|
|
1103
|
+
const values = await client.mGet(keys);
|
|
1104
|
+
return values.map((v: string | null) => v ?? undefined);
|
|
1105
|
+
},
|
|
1082
1106
|
set: async (key: string, value: string, ttl?: number) => {
|
|
1083
1107
|
if (typeof ttl === "number") {
|
|
1084
1108
|
await client.set(key, value, { EX: Math.max(1, ttl) });
|
|
@@ -1261,6 +1285,18 @@ class DynamoDbConversationStore extends KeyValueConversationStoreBase {
|
|
|
1261
1285
|
}),
|
|
1262
1286
|
);
|
|
1263
1287
|
},
|
|
1288
|
+
mget: async (keys: string[]) => {
|
|
1289
|
+
if (keys.length === 0) return [];
|
|
1290
|
+
return Promise.all(keys.map(async (key) => {
|
|
1291
|
+
const result = (await client.send(
|
|
1292
|
+
new client.GetItemCommand({
|
|
1293
|
+
TableName: this.table,
|
|
1294
|
+
Key: { runId: { S: key } },
|
|
1295
|
+
}),
|
|
1296
|
+
)) as { Item?: { value?: { S?: string } } };
|
|
1297
|
+
return result.Item?.value?.S;
|
|
1298
|
+
}));
|
|
1299
|
+
},
|
|
1264
1300
|
del: async (key: string) => {
|
|
1265
1301
|
await client.send(
|
|
1266
1302
|
new client.DeleteItemCommand({
|