@vellumai/assistant 0.5.2 → 0.5.3
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/ARCHITECTURE.md +109 -0
- package/docs/skills.md +100 -0
- package/package.json +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
- package/src/__tests__/conversation-wipe.test.ts +226 -0
- package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
- package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
- package/src/__tests__/inline-command-runner.test.ts +311 -0
- package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
- package/src/__tests__/list-messages-attachments.test.ts +96 -0
- package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
- package/src/__tests__/memory-brief-time.test.ts +285 -0
- package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
- package/src/__tests__/memory-chunk-archive.test.ts +400 -0
- package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
- package/src/__tests__/memory-episode-archive.test.ts +370 -0
- package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
- package/src/__tests__/memory-observation-archive.test.ts +375 -0
- package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
- package/src/__tests__/memory-recall-quality.test.ts +2 -2
- package/src/__tests__/memory-reducer-store.test.ts +728 -0
- package/src/__tests__/memory-reducer-types.test.ts +699 -0
- package/src/__tests__/memory-reducer.test.ts +698 -0
- package/src/__tests__/memory-regressions.test.ts +6 -4
- package/src/__tests__/memory-simplified-config.test.ts +281 -0
- package/src/__tests__/parse-identity-fields.test.ts +129 -0
- package/src/__tests__/skill-load-inline-command.test.ts +598 -0
- package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
- package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
- package/src/__tests__/skills-transitive-hash.test.ts +333 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +8 -8
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +1 -0
- package/src/config/raw-config-utils.ts +28 -0
- package/src/config/schema.ts +12 -0
- package/src/config/schemas/memory-simplified.ts +101 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/skills.ts +50 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -3
- package/src/daemon/conversation-agent-loop.ts +71 -1
- package/src/daemon/conversation-lifecycle.ts +11 -1
- package/src/daemon/conversation-runtime-assembly.ts +2 -1
- package/src/daemon/conversation-surfaces.ts +31 -8
- package/src/daemon/conversation.ts +40 -23
- package/src/daemon/handlers/config-embeddings.ts +10 -2
- package/src/daemon/handlers/config-model.ts +0 -9
- package/src/daemon/handlers/identity.ts +12 -1
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/conversations.ts +0 -1
- package/src/daemon/server.ts +1 -1
- package/src/followups/followup-store.ts +47 -1
- package/src/memory/archive-store.ts +400 -0
- package/src/memory/brief-formatting.ts +33 -0
- package/src/memory/brief-open-loops.ts +266 -0
- package/src/memory/brief-time.ts +161 -0
- package/src/memory/brief.ts +75 -0
- package/src/memory/conversation-crud.ts +245 -101
- package/src/memory/db-init.ts +12 -0
- package/src/memory/indexer.ts +106 -15
- package/src/memory/job-handlers/embedding.test.ts +1 -0
- package/src/memory/job-handlers/embedding.ts +83 -0
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +6 -0
- package/src/memory/jobs-worker.ts +12 -0
- package/src/memory/migrations/185-memory-brief-state.ts +52 -0
- package/src/memory/migrations/186-memory-archive.ts +109 -0
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/qdrant-client.ts +23 -4
- package/src/memory/reducer-store.ts +271 -0
- package/src/memory/reducer-types.ts +99 -0
- package/src/memory/reducer.ts +453 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/index.ts +2 -0
- package/src/memory/schema/memory-archive.ts +121 -0
- package/src/memory/schema/memory-brief.ts +55 -0
- package/src/memory/search/semantic.ts +17 -4
- package/src/oauth/oauth-store.ts +3 -1
- package/src/permissions/checker.ts +89 -6
- package/src/permissions/defaults.ts +14 -0
- package/src/runtime/routes/conversation-management-routes.ts +6 -0
- package/src/runtime/routes/conversation-query-routes.ts +7 -0
- package/src/runtime/routes/conversation-routes.ts +52 -5
- package/src/runtime/routes/identity-routes.ts +2 -35
- package/src/runtime/routes/llm-context-normalization.ts +14 -1
- package/src/runtime/routes/memory-item-routes.ts +90 -5
- package/src/runtime/routes/secret-routes.ts +2 -0
- package/src/runtime/routes/surface-action-routes.ts +68 -1
- package/src/schedule/schedule-store.ts +21 -0
- package/src/skills/inline-command-expansions.ts +204 -0
- package/src/skills/inline-command-render.ts +127 -0
- package/src/skills/inline-command-runner.ts +242 -0
- package/src/skills/transitive-version-hash.ts +88 -0
- package/src/tasks/task-store.ts +43 -1
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/skills/load.ts +140 -6
- package/src/util/platform.ts +18 -0
- package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +1 -1
- package/src/workspace/migrations/registry.ts +1 -1
package/src/config/skills.ts
CHANGED
|
@@ -25,10 +25,15 @@ import {
|
|
|
25
25
|
userMessage,
|
|
26
26
|
} from "../providers/provider-send-message.js";
|
|
27
27
|
import { parseFrontmatterFields } from "../skills/frontmatter.js";
|
|
28
|
+
import type { InlineCommandExpansion } from "../skills/inline-command-expansions.js";
|
|
29
|
+
import { parseInlineCommandExpansions } from "../skills/inline-command-expansions.js";
|
|
28
30
|
import { parseToolManifestFile } from "../skills/tool-manifest.js";
|
|
29
31
|
import { computeSkillVersionHash } from "../skills/version-hash.js";
|
|
30
32
|
import { getLogger } from "../util/logger.js";
|
|
31
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
getWorkspaceDirDisplay,
|
|
35
|
+
getWorkspaceSkillsDir,
|
|
36
|
+
} from "../util/platform.js";
|
|
32
37
|
import { isAssistantFeatureFlagEnabled } from "./assistant-feature-flags.js";
|
|
33
38
|
import { getConfig } from "./loader.js";
|
|
34
39
|
|
|
@@ -80,6 +85,8 @@ export interface SkillSummary {
|
|
|
80
85
|
activationHints?: string[];
|
|
81
86
|
/** Conditions under which this skill should NOT be loaded. */
|
|
82
87
|
avoidWhen?: string[];
|
|
88
|
+
/** Parsed inline command expansion descriptors (`!\`command\``) found in the skill body. */
|
|
89
|
+
inlineCommandExpansions?: InlineCommandExpansion[];
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
export interface SkillDefinition extends SkillSummary {
|
|
@@ -198,6 +205,7 @@ interface ParsedFrontmatter {
|
|
|
198
205
|
featureFlag?: string;
|
|
199
206
|
activationHints?: string[];
|
|
200
207
|
avoidWhen?: string[];
|
|
208
|
+
inlineCommandExpansions?: InlineCommandExpansion[];
|
|
201
209
|
}
|
|
202
210
|
|
|
203
211
|
function normalizeStringArray(raw: unknown): string[] | undefined {
|
|
@@ -302,16 +310,29 @@ function parseFrontmatter(
|
|
|
302
310
|
const activationHints = normalizeStringArray(vellum?.["activation-hints"]);
|
|
303
311
|
const avoidWhen = normalizeStringArray(vellum?.["avoid-when"]);
|
|
304
312
|
|
|
313
|
+
const strippedBody = stripCommentLines(body);
|
|
314
|
+
|
|
315
|
+
// Parse inline command expansions from the body (after frontmatter/comment stripping)
|
|
316
|
+
const expansionResult = parseInlineCommandExpansions(strippedBody);
|
|
317
|
+
const inlineCommandExpansions =
|
|
318
|
+
expansionResult.expansions.length > 0
|
|
319
|
+
? expansionResult.expansions
|
|
320
|
+
: undefined;
|
|
321
|
+
|
|
322
|
+
// Fail closed: if there are malformed tokens, log and exclude from parsed expansions
|
|
323
|
+
// (errors are already logged inside parseInlineCommandExpansions)
|
|
324
|
+
|
|
305
325
|
return {
|
|
306
326
|
name,
|
|
307
327
|
displayName,
|
|
308
328
|
description,
|
|
309
|
-
body:
|
|
329
|
+
body: strippedBody,
|
|
310
330
|
emoji,
|
|
311
331
|
includes,
|
|
312
332
|
featureFlag,
|
|
313
333
|
activationHints,
|
|
314
334
|
avoidWhen,
|
|
335
|
+
inlineCommandExpansions,
|
|
315
336
|
};
|
|
316
337
|
}
|
|
317
338
|
|
|
@@ -466,6 +487,7 @@ function readSkillFromDirectory(
|
|
|
466
487
|
featureFlag: parsed.featureFlag,
|
|
467
488
|
activationHints: parsed.activationHints,
|
|
468
489
|
avoidWhen: parsed.avoidWhen,
|
|
490
|
+
inlineCommandExpansions: parsed.inlineCommandExpansions,
|
|
469
491
|
};
|
|
470
492
|
} catch (err) {
|
|
471
493
|
log.warn({ err, skillFilePath }, "Failed to read skill file");
|
|
@@ -516,6 +538,7 @@ function readBundledSkillFromDirectory(
|
|
|
516
538
|
featureFlag: parsed.featureFlag,
|
|
517
539
|
activationHints: parsed.activationHints,
|
|
518
540
|
avoidWhen: parsed.avoidWhen,
|
|
541
|
+
inlineCommandExpansions: parsed.inlineCommandExpansions,
|
|
519
542
|
};
|
|
520
543
|
} catch (err) {
|
|
521
544
|
log.warn({ err, skillFilePath }, "Failed to read bundled skill file");
|
|
@@ -574,6 +597,7 @@ function loadBundledSkills(): SkillSummary[] {
|
|
|
574
597
|
featureFlag: skill.featureFlag,
|
|
575
598
|
activationHints: skill.activationHints,
|
|
576
599
|
avoidWhen: skill.avoidWhen,
|
|
600
|
+
inlineCommandExpansions: skill.inlineCommandExpansions,
|
|
577
601
|
});
|
|
578
602
|
}
|
|
579
603
|
|
|
@@ -710,6 +734,7 @@ function skillSummaryFromDefinition(
|
|
|
710
734
|
featureFlag: skill.featureFlag,
|
|
711
735
|
activationHints: skill.activationHints,
|
|
712
736
|
avoidWhen: skill.avoidWhen,
|
|
737
|
+
inlineCommandExpansions: skill.inlineCommandExpansions,
|
|
713
738
|
};
|
|
714
739
|
}
|
|
715
740
|
|
|
@@ -760,6 +785,7 @@ export function loadSkillCatalog(
|
|
|
760
785
|
toolManifest: detectToolManifest(directory),
|
|
761
786
|
includes: parsed.includes,
|
|
762
787
|
featureFlag: parsed.featureFlag,
|
|
788
|
+
inlineCommandExpansions: parsed.inlineCommandExpansions,
|
|
763
789
|
});
|
|
764
790
|
} catch (err) {
|
|
765
791
|
log.warn({ err, directory }, "Failed to read skill from extraDirs");
|
|
@@ -854,6 +880,7 @@ export function loadSkillCatalog(
|
|
|
854
880
|
toolManifest: detectToolManifest(directory),
|
|
855
881
|
includes: parsed.includes,
|
|
856
882
|
featureFlag: parsed.featureFlag,
|
|
883
|
+
inlineCommandExpansions: parsed.inlineCommandExpansions,
|
|
857
884
|
};
|
|
858
885
|
|
|
859
886
|
if (seenIds.has(id)) {
|
|
@@ -1001,8 +1028,28 @@ function loadSkillDefinition(skill: SkillSummary): SkillLookupResult {
|
|
|
1001
1028
|
}
|
|
1002
1029
|
// Replace {baseDir} placeholders with the actual skill directory path
|
|
1003
1030
|
loaded.body = loaded.body.replaceAll("{baseDir}", loaded.directoryPath);
|
|
1031
|
+
// Replace {workspaceDir} placeholders with the runtime workspace display path
|
|
1032
|
+
loaded.body = loaded.body.replaceAll(
|
|
1033
|
+
"{workspaceDir}",
|
|
1034
|
+
getWorkspaceDirDisplay(),
|
|
1035
|
+
);
|
|
1004
1036
|
// Strip feature-gated sections based on assistant feature flags
|
|
1005
1037
|
loaded.body = applyFeatureGatedSections(loaded.body);
|
|
1038
|
+
|
|
1039
|
+
// Re-parse inline command expansions after placeholder substitution.
|
|
1040
|
+
// The initial parse (during SKILL.md parsing) produces byte offsets against
|
|
1041
|
+
// the pre-substitution body. Since {baseDir} and {workspaceDir} replacements
|
|
1042
|
+
// change the body length, those offsets become stale. Re-parsing ensures the
|
|
1043
|
+
// offsets match the final body that renderInlineCommands will operate on.
|
|
1044
|
+
if (
|
|
1045
|
+
loaded.inlineCommandExpansions &&
|
|
1046
|
+
loaded.inlineCommandExpansions.length > 0
|
|
1047
|
+
) {
|
|
1048
|
+
const reparse = parseInlineCommandExpansions(loaded.body);
|
|
1049
|
+
loaded.inlineCommandExpansions =
|
|
1050
|
+
reparse.expansions.length > 0 ? reparse.expansions : undefined;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1006
1053
|
return { skill: loaded };
|
|
1007
1054
|
}
|
|
1008
1055
|
|
|
@@ -1021,8 +1068,7 @@ export function resolveSkillSelector(
|
|
|
1021
1068
|
const catalog = loadSkillCatalog(workspaceSkillsDir);
|
|
1022
1069
|
if (catalog.length === 0) {
|
|
1023
1070
|
return {
|
|
1024
|
-
error:
|
|
1025
|
-
"No skills are available. Configure ~/.vellum/workspace/skills/SKILLS.md or add skill directories.",
|
|
1071
|
+
error: `No skills are available. Configure ${getWorkspaceDirDisplay()}/skills/SKILLS.md or add skill directories.`,
|
|
1026
1072
|
errorCode: "empty_catalog",
|
|
1027
1073
|
};
|
|
1028
1074
|
}
|
|
@@ -279,7 +279,12 @@ export function handleToolUse(
|
|
|
279
279
|
state.toolCallTimestamps.set(event.id, { startedAt: Date.now() });
|
|
280
280
|
state.currentToolUseId = event.id;
|
|
281
281
|
state.currentTurnToolUseIds.push(event.id);
|
|
282
|
-
const statusText =
|
|
282
|
+
const statusText =
|
|
283
|
+
event.name === "skill_execute" &&
|
|
284
|
+
typeof event.input.activity === "string" &&
|
|
285
|
+
event.input.activity.length > 0
|
|
286
|
+
? event.input.activity
|
|
287
|
+
: `Running ${friendlyToolName(event.name)}`;
|
|
283
288
|
deps.ctx.emitActivityState(
|
|
284
289
|
"tool_running",
|
|
285
290
|
"tool_use_start",
|
|
@@ -398,8 +403,8 @@ export function handleInputJsonDelta(
|
|
|
398
403
|
event: Extract<AgentEvent, { type: "input_json_delta" }>,
|
|
399
404
|
): void {
|
|
400
405
|
// Only forward input deltas for app tools — the client only uses this
|
|
401
|
-
// stream for app_create
|
|
402
|
-
//
|
|
406
|
+
// stream for app_create code previews. Non-app tools would send large
|
|
407
|
+
// cumulative JSON on every delta with no benefit.
|
|
403
408
|
if (!APP_TOOL_NAMES.has(event.toolName)) return;
|
|
404
409
|
deps.onEvent({
|
|
405
410
|
type: "tool_input_delta",
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "../instrument.js";
|
|
34
34
|
import { commitAppTurnChanges } from "../memory/app-git-service.js";
|
|
35
35
|
import { getApp, listAppFiles, resolveAppDir } from "../memory/app-store.js";
|
|
36
|
+
import { insertCompactionEpisode } from "../memory/archive-store.js";
|
|
36
37
|
import {
|
|
37
38
|
addMessage,
|
|
38
39
|
deleteMessageById,
|
|
@@ -208,7 +209,17 @@ export interface AgentLoopConversationContext {
|
|
|
208
209
|
currentPage?: string;
|
|
209
210
|
readonly surfaceState: Map<
|
|
210
211
|
string,
|
|
211
|
-
{
|
|
212
|
+
{
|
|
213
|
+
surfaceType: SurfaceType;
|
|
214
|
+
data: SurfaceData;
|
|
215
|
+
title?: string;
|
|
216
|
+
actions?: Array<{
|
|
217
|
+
id: string;
|
|
218
|
+
label: string;
|
|
219
|
+
style?: string;
|
|
220
|
+
data?: Record<string, unknown>;
|
|
221
|
+
}>;
|
|
222
|
+
}
|
|
212
223
|
>;
|
|
213
224
|
pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
|
|
214
225
|
surfaceActionRequestIds: Set<string>;
|
|
@@ -503,6 +514,12 @@ export async function runAgentLoopImpl(
|
|
|
503
514
|
compacted.summaryText,
|
|
504
515
|
ctx.contextCompactedMessageCount,
|
|
505
516
|
);
|
|
517
|
+
dualWriteCompactionEpisode(
|
|
518
|
+
ctx.conversationId,
|
|
519
|
+
ctx.memoryPolicy.scopeId,
|
|
520
|
+
compacted.summaryText,
|
|
521
|
+
compacted.summaryOutputTokens,
|
|
522
|
+
);
|
|
506
523
|
onEvent({
|
|
507
524
|
type: "context_compacted",
|
|
508
525
|
previousEstimatedInputTokens: compacted.previousEstimatedInputTokens,
|
|
@@ -770,6 +787,12 @@ export async function runAgentLoopImpl(
|
|
|
770
787
|
step.compactionResult.summaryText,
|
|
771
788
|
ctx.contextCompactedMessageCount,
|
|
772
789
|
);
|
|
790
|
+
dualWriteCompactionEpisode(
|
|
791
|
+
ctx.conversationId,
|
|
792
|
+
ctx.memoryPolicy.scopeId,
|
|
793
|
+
step.compactionResult.summaryText,
|
|
794
|
+
step.compactionResult.summaryOutputTokens,
|
|
795
|
+
);
|
|
773
796
|
onEvent({
|
|
774
797
|
type: "context_compacted",
|
|
775
798
|
previousEstimatedInputTokens:
|
|
@@ -954,6 +977,12 @@ export async function runAgentLoopImpl(
|
|
|
954
977
|
midLoopCompact.summaryText,
|
|
955
978
|
ctx.contextCompactedMessageCount,
|
|
956
979
|
);
|
|
980
|
+
dualWriteCompactionEpisode(
|
|
981
|
+
ctx.conversationId,
|
|
982
|
+
ctx.memoryPolicy.scopeId,
|
|
983
|
+
midLoopCompact.summaryText,
|
|
984
|
+
midLoopCompact.summaryOutputTokens,
|
|
985
|
+
);
|
|
957
986
|
onEvent({
|
|
958
987
|
type: "context_compacted",
|
|
959
988
|
previousEstimatedInputTokens:
|
|
@@ -1150,6 +1179,12 @@ export async function runAgentLoopImpl(
|
|
|
1150
1179
|
step.compactionResult.summaryText,
|
|
1151
1180
|
ctx.contextCompactedMessageCount,
|
|
1152
1181
|
);
|
|
1182
|
+
dualWriteCompactionEpisode(
|
|
1183
|
+
ctx.conversationId,
|
|
1184
|
+
ctx.memoryPolicy.scopeId,
|
|
1185
|
+
step.compactionResult.summaryText,
|
|
1186
|
+
step.compactionResult.summaryOutputTokens,
|
|
1187
|
+
);
|
|
1153
1188
|
onEvent({
|
|
1154
1189
|
type: "context_compacted",
|
|
1155
1190
|
previousEstimatedInputTokens:
|
|
@@ -1257,6 +1292,12 @@ export async function runAgentLoopImpl(
|
|
|
1257
1292
|
emergencyCompact.summaryText,
|
|
1258
1293
|
ctx.contextCompactedMessageCount,
|
|
1259
1294
|
);
|
|
1295
|
+
dualWriteCompactionEpisode(
|
|
1296
|
+
ctx.conversationId,
|
|
1297
|
+
ctx.memoryPolicy.scopeId,
|
|
1298
|
+
emergencyCompact.summaryText,
|
|
1299
|
+
emergencyCompact.summaryOutputTokens,
|
|
1300
|
+
);
|
|
1260
1301
|
onEvent({
|
|
1261
1302
|
type: "context_compacted",
|
|
1262
1303
|
previousEstimatedInputTokens:
|
|
@@ -1361,6 +1402,12 @@ export async function runAgentLoopImpl(
|
|
|
1361
1402
|
emergencyCompact.summaryText,
|
|
1362
1403
|
ctx.contextCompactedMessageCount,
|
|
1363
1404
|
);
|
|
1405
|
+
dualWriteCompactionEpisode(
|
|
1406
|
+
ctx.conversationId,
|
|
1407
|
+
ctx.memoryPolicy.scopeId,
|
|
1408
|
+
emergencyCompact.summaryText,
|
|
1409
|
+
emergencyCompact.summaryOutputTokens,
|
|
1410
|
+
);
|
|
1364
1411
|
onEvent({
|
|
1365
1412
|
type: "context_compacted",
|
|
1366
1413
|
previousEstimatedInputTokens:
|
|
@@ -1826,3 +1873,26 @@ function collapseRawResponses(rawResponses?: unknown[]): unknown | undefined {
|
|
|
1826
1873
|
if (!rawResponses || rawResponses.length === 0) return undefined;
|
|
1827
1874
|
return rawResponses.length === 1 ? rawResponses[0] : rawResponses;
|
|
1828
1875
|
}
|
|
1876
|
+
|
|
1877
|
+
/**
|
|
1878
|
+
* Dual-write a compaction summary as an archive episode so it becomes
|
|
1879
|
+
* searchable via vector recall. Called after each successful compaction
|
|
1880
|
+
* that produces a new summary.
|
|
1881
|
+
*/
|
|
1882
|
+
function dualWriteCompactionEpisode(
|
|
1883
|
+
conversationId: string,
|
|
1884
|
+
scopeId: string,
|
|
1885
|
+
summaryText: string,
|
|
1886
|
+
summaryOutputTokens: number,
|
|
1887
|
+
): void {
|
|
1888
|
+
const now = Date.now();
|
|
1889
|
+
insertCompactionEpisode({
|
|
1890
|
+
conversationId,
|
|
1891
|
+
scopeId,
|
|
1892
|
+
title: truncate(summaryText, 120, ""),
|
|
1893
|
+
summary: summaryText,
|
|
1894
|
+
tokenEstimate: summaryOutputTokens,
|
|
1895
|
+
startAt: now,
|
|
1896
|
+
endAt: now,
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
@@ -123,7 +123,17 @@ export interface AbortContext {
|
|
|
123
123
|
surfaceActionRequestIds: Set<string>;
|
|
124
124
|
surfaceState: Map<
|
|
125
125
|
string,
|
|
126
|
-
{
|
|
126
|
+
{
|
|
127
|
+
surfaceType: SurfaceType;
|
|
128
|
+
data: SurfaceData;
|
|
129
|
+
title?: string;
|
|
130
|
+
actions?: Array<{
|
|
131
|
+
id: string;
|
|
132
|
+
label: string;
|
|
133
|
+
style?: string;
|
|
134
|
+
data?: Record<string, unknown>;
|
|
135
|
+
}>;
|
|
136
|
+
}
|
|
127
137
|
>;
|
|
128
138
|
accumulatedSurfaceState: Map<string, Record<string, unknown>>;
|
|
129
139
|
readonly queue: MessageQueue;
|
|
@@ -306,7 +306,7 @@ export function injectActiveSurfaceContext(
|
|
|
306
306
|
'PREREQUISITE: If `app_refresh` is not yet available, call `skill_load` with `id: "app-builder"` first to load it.',
|
|
307
307
|
"",
|
|
308
308
|
"RULES FOR WORKSPACE MODIFICATION:",
|
|
309
|
-
`1. Use \`file_edit\` to make surgical changes to app files. The file path is
|
|
309
|
+
`1. Use \`file_edit\` to make surgical changes to app files. The file path is \`${getAppDirPath(ctx.appId)}/<path>\`.`,
|
|
310
310
|
"2. Use `file_write` to create new files or rewrite files.",
|
|
311
311
|
"3. Use `file_read` to read any file with line numbers before editing.",
|
|
312
312
|
"4. Use `bash ls` to see all files in the app directory.",
|
|
@@ -961,6 +961,7 @@ const RUNTIME_INJECTION_PREFIXES = [
|
|
|
961
961
|
"<inbound_actor_context>",
|
|
962
962
|
"<interface_turn_context>",
|
|
963
963
|
"<turn_context>",
|
|
964
|
+
"<memory_brief>",
|
|
964
965
|
"<memory_context __injected>",
|
|
965
966
|
"<memory_context>", // backward-compat: strip legacy blocks from pre-__injected history
|
|
966
967
|
"<voice_call_control>",
|
|
@@ -172,6 +172,7 @@ export interface SurfaceConversationContext {
|
|
|
172
172
|
emit(type: string, message: string, meta?: Record<string, unknown>): void;
|
|
173
173
|
};
|
|
174
174
|
sendToClient(msg: ServerMessage): void;
|
|
175
|
+
broadcastToAllClients?(msg: ServerMessage): void;
|
|
175
176
|
pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
|
|
176
177
|
lastSurfaceAction: Map<
|
|
177
178
|
string,
|
|
@@ -617,10 +618,18 @@ export function handleSurfaceAction(
|
|
|
617
618
|
const prompt =
|
|
618
619
|
isRelay && typeof data?.prompt === "string" ? data.prompt.trim() : "";
|
|
619
620
|
|
|
621
|
+
// Read accumulated state once — used by both relay and custom action paths.
|
|
622
|
+
const accState = ctx.accumulatedSurfaceState.get(surfaceId);
|
|
623
|
+
const hasAccState = accState && Object.keys(accState).length > 0;
|
|
624
|
+
|
|
620
625
|
let content: string;
|
|
621
626
|
let displayContent: string | undefined;
|
|
622
627
|
if (prompt) {
|
|
623
628
|
content = prompt;
|
|
629
|
+
// Re-append accumulated state so the LLM sees it, matching the pending path.
|
|
630
|
+
if (hasAccState) {
|
|
631
|
+
content += `\n\nAccumulated surface state: ${JSON.stringify(accState)}`;
|
|
632
|
+
}
|
|
624
633
|
} else {
|
|
625
634
|
// Custom action from an app (e.g. sendAction('answer_selected', {...}))
|
|
626
635
|
const summary = actionId
|
|
@@ -630,17 +639,20 @@ export function handleSurfaceAction(
|
|
|
630
639
|
if (data && Object.keys(data).length > 0) {
|
|
631
640
|
content += `\n\nAction data: ${JSON.stringify(data)}`;
|
|
632
641
|
}
|
|
633
|
-
|
|
634
|
-
if (accState && Object.keys(accState).length > 0) {
|
|
642
|
+
if (hasAccState) {
|
|
635
643
|
content += `\n\nAccumulated surface state: ${JSON.stringify(accState)}`;
|
|
636
|
-
ctx.accumulatedSurfaceState.delete(surfaceId);
|
|
637
644
|
}
|
|
638
645
|
displayContent = summary;
|
|
639
646
|
}
|
|
640
647
|
|
|
641
648
|
const requestId = uuid();
|
|
642
649
|
ctx.surfaceActionRequestIds.add(requestId);
|
|
643
|
-
|
|
650
|
+
// Use broadcastToAllClients (publishes to the SSE event hub) instead of
|
|
651
|
+
// sendToClient, which is reset to a no-op between HTTP requests. Without
|
|
652
|
+
// this, surface action responses are persisted to DB but never reach the
|
|
653
|
+
// client's SSE stream.
|
|
654
|
+
const emit = ctx.broadcastToAllClients ?? ctx.sendToClient.bind(ctx);
|
|
655
|
+
const onEvent = (msg: ServerMessage) => emit(msg);
|
|
644
656
|
|
|
645
657
|
ctx.traceEmitter.emit("request_received", "Surface action received", {
|
|
646
658
|
requestId,
|
|
@@ -665,10 +677,16 @@ export function handleSurfaceAction(
|
|
|
665
677
|
return;
|
|
666
678
|
}
|
|
667
679
|
|
|
680
|
+
// One-shot: clear accumulated state now that the message has been accepted.
|
|
681
|
+
// Deferred until after rejection check so state is preserved for retry on rejection.
|
|
682
|
+
if (hasAccState) {
|
|
683
|
+
ctx.accumulatedSurfaceState.delete(surfaceId);
|
|
684
|
+
}
|
|
685
|
+
|
|
668
686
|
// Echo the prompt to the client so it appears in the chat UI.
|
|
669
687
|
// Deferred until after rejection check to avoid ghost messages.
|
|
670
688
|
if (prompt) {
|
|
671
|
-
|
|
689
|
+
emit({
|
|
672
690
|
type: "user_message_echo",
|
|
673
691
|
text: prompt,
|
|
674
692
|
conversationId: ctx.conversationId,
|
|
@@ -768,11 +786,16 @@ export function handleSurfaceAction(
|
|
|
768
786
|
surfaceData,
|
|
769
787
|
);
|
|
770
788
|
|
|
789
|
+
// Use broadcastToAllClients so events reach the SSE hub — sendToClient is
|
|
790
|
+
// reset to a no-op between HTTP requests (see history-restored path for
|
|
791
|
+
// full rationale).
|
|
792
|
+
const emit = ctx.broadcastToAllClients ?? ctx.sendToClient.bind(ctx);
|
|
793
|
+
|
|
771
794
|
// Forms are one-shot surfaces — auto-complete immediately so the client
|
|
772
795
|
// transitions from the "Submitting…" spinner to a completion chip without
|
|
773
796
|
// requiring the LLM to call ui_dismiss.
|
|
774
797
|
if (pending.surfaceType === "form") {
|
|
775
|
-
|
|
798
|
+
emit({
|
|
776
799
|
type: "ui_surface_complete",
|
|
777
800
|
conversationId: ctx.conversationId,
|
|
778
801
|
surfaceId,
|
|
@@ -833,7 +856,7 @@ export function handleSurfaceAction(
|
|
|
833
856
|
|
|
834
857
|
const requestId = uuid();
|
|
835
858
|
ctx.surfaceActionRequestIds.add(requestId);
|
|
836
|
-
const onEvent = (msg: ServerMessage) =>
|
|
859
|
+
const onEvent = (msg: ServerMessage) => emit(msg);
|
|
837
860
|
|
|
838
861
|
ctx.traceEmitter.emit("request_received", "Surface action received", {
|
|
839
862
|
requestId,
|
|
@@ -866,7 +889,7 @@ export function handleSurfaceAction(
|
|
|
866
889
|
// Echo the user's prompt to the client so it appears in the chat UI.
|
|
867
890
|
// Deferred until after rejection check to avoid ghost messages.
|
|
868
891
|
if (shouldRelayPrompt && prompt) {
|
|
869
|
-
|
|
892
|
+
emit({
|
|
870
893
|
type: "user_message_echo",
|
|
871
894
|
text: prompt,
|
|
872
895
|
conversationId: ctx.conversationId,
|
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
import { registerToolTraceListener } from "../events/tool-trace-listener.js";
|
|
39
39
|
import { getHookManager } from "../hooks/manager.js";
|
|
40
40
|
import { resolveCanonicalGuardianRequest } from "../memory/canonical-guardian-store.js";
|
|
41
|
-
import { getMessages } from "../memory/conversation-crud.js";
|
|
42
41
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
43
42
|
import { SecretPrompter } from "../permissions/secret-prompter.js";
|
|
44
43
|
import { patternMatchesCandidate } from "../permissions/trust-store.js";
|
|
@@ -196,13 +195,24 @@ export class Conversation {
|
|
|
196
195
|
>();
|
|
197
196
|
/** @internal */ surfaceState = new Map<
|
|
198
197
|
string,
|
|
199
|
-
{
|
|
198
|
+
{
|
|
199
|
+
surfaceType: SurfaceType;
|
|
200
|
+
data: SurfaceData;
|
|
201
|
+
title?: string;
|
|
202
|
+
actions?: Array<{
|
|
203
|
+
id: string;
|
|
204
|
+
label: string;
|
|
205
|
+
style?: string;
|
|
206
|
+
data?: Record<string, unknown>;
|
|
207
|
+
}>;
|
|
208
|
+
}
|
|
200
209
|
>();
|
|
201
210
|
/** @internal */ surfaceUndoStacks = new Map<string, string[]>();
|
|
202
211
|
/** @internal */ accumulatedSurfaceState = new Map<
|
|
203
212
|
string,
|
|
204
213
|
Record<string, unknown>
|
|
205
214
|
>();
|
|
215
|
+
/** @internal */ broadcastToAllClients?: (msg: ServerMessage) => void;
|
|
206
216
|
/** @internal */ withSurface = createSurfaceMutex();
|
|
207
217
|
/** @internal */ currentTurnSurfaces: Array<{
|
|
208
218
|
surfaceId: string;
|
|
@@ -249,6 +259,7 @@ export class Conversation {
|
|
|
249
259
|
this.provider = provider;
|
|
250
260
|
this.workingDir = workingDir;
|
|
251
261
|
this.sendToClient = sendToClient;
|
|
262
|
+
this.broadcastToAllClients = broadcastToAllClients;
|
|
252
263
|
this.memoryPolicy = memoryPolicy
|
|
253
264
|
? { ...memoryPolicy }
|
|
254
265
|
: { ...DEFAULT_MEMORY_POLICY };
|
|
@@ -386,30 +397,36 @@ export class Conversation {
|
|
|
386
397
|
}
|
|
387
398
|
|
|
388
399
|
/**
|
|
389
|
-
* Scan
|
|
390
|
-
*
|
|
391
|
-
*
|
|
400
|
+
* Scan loaded conversation history for ui_surface content blocks and
|
|
401
|
+
* populate surfaceState so that findConversationBySurfaceId works for
|
|
402
|
+
* surfaces restored from history (e.g. after daemon restart).
|
|
403
|
+
*
|
|
404
|
+
* Only scans live (non-compacted) messages in this.messages — not all DB
|
|
405
|
+
* rows — because surface IDs are not globally unique and restoring stale
|
|
406
|
+
* compacted surfaces would let findConversationBySurfaceId route actions
|
|
407
|
+
* to the wrong conversation.
|
|
392
408
|
*/
|
|
393
409
|
private restoreSurfaceStateFromHistory(): void {
|
|
394
|
-
|
|
395
|
-
for (const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
+
this.surfaceState.clear();
|
|
411
|
+
for (const msg of this.messages) {
|
|
412
|
+
if (!Array.isArray(msg.content)) continue;
|
|
413
|
+
for (const block of msg.content) {
|
|
414
|
+
const b = block as unknown as Record<string, unknown>;
|
|
415
|
+
if (b.type === "ui_surface" && typeof b.surfaceId === "string") {
|
|
416
|
+
this.surfaceState.set(b.surfaceId, {
|
|
417
|
+
surfaceType: (b.surfaceType ?? "dynamic_page") as SurfaceType,
|
|
418
|
+
data: (b.data ?? {}) as SurfaceData,
|
|
419
|
+
title: b.title as string | undefined,
|
|
420
|
+
actions: Array.isArray(b.actions)
|
|
421
|
+
? (b.actions as Array<{
|
|
422
|
+
id: string;
|
|
423
|
+
label: string;
|
|
424
|
+
style?: string;
|
|
425
|
+
data?: Record<string, unknown>;
|
|
426
|
+
}>)
|
|
427
|
+
: undefined,
|
|
428
|
+
});
|
|
410
429
|
}
|
|
411
|
-
} catch {
|
|
412
|
-
// Content isn't valid JSON — skip
|
|
413
430
|
}
|
|
414
431
|
}
|
|
415
432
|
}
|
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
loadRawConfig,
|
|
4
4
|
saveRawConfig,
|
|
5
5
|
} from "../../config/loader.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
deleteMemoryEmbeddingField,
|
|
8
|
+
setMemoryEmbeddingField,
|
|
9
|
+
} from "../../config/raw-config-utils.js";
|
|
7
10
|
import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
|
|
8
11
|
import {
|
|
9
12
|
clearEmbeddingBackendCache,
|
|
@@ -118,7 +121,12 @@ export async function setEmbeddingConfig(
|
|
|
118
121
|
if (model !== undefined) {
|
|
119
122
|
const fieldName = PROVIDER_MODEL_FIELD[provider];
|
|
120
123
|
if (fieldName) {
|
|
121
|
-
|
|
124
|
+
if (model === "") {
|
|
125
|
+
// Empty string means "clear override — use schema default"
|
|
126
|
+
deleteMemoryEmbeddingField(raw, fieldName);
|
|
127
|
+
} else {
|
|
128
|
+
setMemoryEmbeddingField(raw, fieldName, model);
|
|
129
|
+
}
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
132
|
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
isProviderAvailable,
|
|
17
17
|
} from "../../providers/provider-availability.js";
|
|
18
18
|
import { initializeProviders } from "../../providers/registry.js";
|
|
19
|
-
import { getMaskedProviderKey } from "../../security/secure-keys.js";
|
|
20
19
|
import type {
|
|
21
20
|
ImageGenModelSetRequest,
|
|
22
21
|
ModelSetRequest,
|
|
@@ -44,7 +43,6 @@ export interface ModelInfo {
|
|
|
44
43
|
configuredProviders?: string[];
|
|
45
44
|
availableModels?: Array<{ id: string; displayName: string }>;
|
|
46
45
|
allProviders?: ProviderCatalogEntry[];
|
|
47
|
-
maskedKeys?: Record<string, string>;
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
/** Return current model configuration. */
|
|
@@ -52,19 +50,12 @@ export async function getModelInfo(): Promise<ModelInfo> {
|
|
|
52
50
|
const config = getConfig();
|
|
53
51
|
const provider = config.services.inference.provider;
|
|
54
52
|
|
|
55
|
-
const maskedKeys: Record<string, string> = {};
|
|
56
|
-
for (const p of VALID_INFERENCE_PROVIDERS) {
|
|
57
|
-
const masked = await getMaskedProviderKey(p);
|
|
58
|
-
if (masked) maskedKeys[p] = masked;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
53
|
return {
|
|
62
54
|
model: config.services.inference.model,
|
|
63
55
|
provider,
|
|
64
56
|
configuredProviders: await getConfiguredProviders(),
|
|
65
57
|
availableModels: PROVIDER_CATALOG.find((p) => p.id === provider)?.models,
|
|
66
58
|
allProviders: PROVIDER_CATALOG,
|
|
67
|
-
maskedKeys,
|
|
68
59
|
};
|
|
69
60
|
}
|
|
70
61
|
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true when the value is a template placeholder that should be treated
|
|
3
|
+
* as empty/unset. Placeholders follow the pattern `_(…)_`, e.g.
|
|
4
|
+
* `_(not yet chosen)_` or `_(not yet established)_`.
|
|
5
|
+
*/
|
|
6
|
+
export function isTemplatePlaceholder(value: string): boolean {
|
|
7
|
+
return value.startsWith("_(") && value.endsWith(")_");
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
export interface IdentityFields {
|
|
2
11
|
name: string;
|
|
3
12
|
role: string;
|
|
@@ -14,7 +23,9 @@ export function parseIdentityFields(content: string): IdentityFields {
|
|
|
14
23
|
const lower = trimmed.toLowerCase();
|
|
15
24
|
const extract = (prefix: string): string | null => {
|
|
16
25
|
if (!lower.startsWith(prefix)) return null;
|
|
17
|
-
|
|
26
|
+
const value = trimmed.split(":**").pop()?.trim() ?? null;
|
|
27
|
+
if (value && isTemplatePlaceholder(value)) return null;
|
|
28
|
+
return value;
|
|
18
29
|
};
|
|
19
30
|
|
|
20
31
|
const name = extract("- **name:**");
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -200,14 +200,22 @@ export async function runDaemon(): Promise<void> {
|
|
|
200
200
|
targetId: itemId,
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
|
+
for (const summaryId of deletedMemory.deletedSummaryIds) {
|
|
204
|
+
enqueueMemoryJob("delete_qdrant_vectors", {
|
|
205
|
+
targetType: "summary",
|
|
206
|
+
targetId: summaryId,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
203
209
|
if (
|
|
204
210
|
deletedMemory.segmentIds.length > 0 ||
|
|
205
|
-
deletedMemory.orphanedItemIds.length > 0
|
|
211
|
+
deletedMemory.orphanedItemIds.length > 0 ||
|
|
212
|
+
deletedMemory.deletedSummaryIds.length > 0
|
|
206
213
|
) {
|
|
207
214
|
log.info(
|
|
208
215
|
{
|
|
209
216
|
segments: deletedMemory.segmentIds.length,
|
|
210
217
|
orphanedItems: deletedMemory.orphanedItemIds.length,
|
|
218
|
+
deletedSummaries: deletedMemory.deletedSummaryIds.length,
|
|
211
219
|
},
|
|
212
220
|
"Enqueued Qdrant vector cleanup jobs for purged private conversations",
|
|
213
221
|
);
|
package/src/daemon/server.ts
CHANGED
|
@@ -1262,7 +1262,7 @@ export class DaemonServer {
|
|
|
1262
1262
|
const appId = surfaceId.slice(appOpenPrefix.length);
|
|
1263
1263
|
for (const c of this.conversations.values()) {
|
|
1264
1264
|
for (const [, state] of c.surfaceState.entries()) {
|
|
1265
|
-
const data = state.data as Record<string, unknown>;
|
|
1265
|
+
const data = state.data as unknown as Record<string, unknown>;
|
|
1266
1266
|
if (data?.appId === appId) {
|
|
1267
1267
|
// Register this surfaceId so subsequent lookups are O(1)
|
|
1268
1268
|
c.surfaceState.set(surfaceId, state);
|