@vellumai/assistant 0.9.0-dev.202606181513.fd39213 → 0.9.0-dev.202606181735.ae6e1d8
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/openapi.yaml +82 -0
- package/package.json +1 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
- package/src/api/events/workflow-completed.ts +53 -0
- package/src/api/events/workflow-leaf-finished.ts +38 -0
- package/src/api/events/workflow-leaf-started.ts +35 -0
- package/src/api/events/workflow-progress.ts +32 -0
- package/src/api/events/workflow-started.ts +31 -0
- package/src/api/index.ts +38 -0
- package/src/api/responses/workflow-journal.ts +53 -0
- package/src/daemon/message-types/workflows.ts +83 -1
- package/src/memory/db-init.ts +2 -0
- package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +134 -0
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +147 -0
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
- package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
- package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
- package/src/plugins/defaults/advisor/config.ts +26 -0
- package/src/plugins/defaults/advisor/consult.ts +76 -0
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +24 -0
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/advisor/package.json +14 -0
- package/src/plugins/defaults/advisor/steering.ts +51 -0
- package/src/plugins/defaults/advisor/tools/advisor.ts +52 -0
- package/src/plugins/defaults/advisor/transcript.ts +76 -0
- package/src/plugins/defaults/index.ts +35 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
- package/src/runtime/routes/workflow-routes.test.ts +225 -1
- package/src/runtime/routes/workflow-routes.ts +131 -1
- package/src/tools/workflows/run-workflow.ts +1 -0
- package/src/workflows/engine.test.ts +175 -1
- package/src/workflows/engine.ts +82 -0
- package/src/workflows/journal-store.test.ts +70 -0
- package/src/workflows/journal-store.ts +18 -3
- package/src/workflows/run-manager.test.ts +171 -3
- package/src/workflows/run-manager.ts +63 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run one advisor consult.
|
|
3
|
+
*
|
|
4
|
+
* Routed entirely through the assistant's own inference: `getConfiguredProvider`
|
|
5
|
+
* resolves the general-purpose `inference` call site (with the advisor profile
|
|
6
|
+
* applied as an `overrideProfile`) to a provider/model/credentials from the
|
|
7
|
+
* configured profiles — managed-proxy or BYOK, no separate API key. The consult
|
|
8
|
+
* then runs a tool-less, capped one-shot completion through `provider.sendMessage`
|
|
9
|
+
* and returns the text.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { LLMCallSite } from "../../../config/schemas/llm.js";
|
|
13
|
+
import {
|
|
14
|
+
extractAllText,
|
|
15
|
+
getConfiguredProvider,
|
|
16
|
+
userMessage,
|
|
17
|
+
} from "../../../providers/provider-send-message.js";
|
|
18
|
+
import type { Message } from "../../../providers/types.js";
|
|
19
|
+
import { ADVISOR_CONFIG } from "./config.js";
|
|
20
|
+
import { advisorRequestText, buildAdvisorSystem } from "./steering.js";
|
|
21
|
+
import { toAdvisorMessages } from "./transcript.js";
|
|
22
|
+
|
|
23
|
+
// The general-purpose call site; the model is selected via `overrideProfile`.
|
|
24
|
+
const ADVISOR_CALL_SITE: LLMCallSite = "inference";
|
|
25
|
+
|
|
26
|
+
export interface ConsultParams {
|
|
27
|
+
systemPrompt: string | null;
|
|
28
|
+
messages: ReadonlyArray<Message>;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Combine the caller's signal with a consult timeout. */
|
|
33
|
+
function withTimeout(signal: AbortSignal | undefined, ms: number): AbortSignal {
|
|
34
|
+
const timeout = AbortSignal.timeout(ms);
|
|
35
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the advisor's guidance text, or a short benign notice when the
|
|
40
|
+
* advisor can't run. Callers should surface the string as a non-error tool
|
|
41
|
+
* result so the executor continues regardless.
|
|
42
|
+
*/
|
|
43
|
+
export async function consultAdvisor(params: ConsultParams): Promise<string> {
|
|
44
|
+
const history = toAdvisorMessages(params.messages);
|
|
45
|
+
if (history.length === 0) {
|
|
46
|
+
return "(advisor: no conversation context is available yet)";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const provider = await getConfiguredProvider(ADVISOR_CALL_SITE, {
|
|
50
|
+
overrideProfile: ADVISOR_CONFIG.profile,
|
|
51
|
+
});
|
|
52
|
+
if (!provider) {
|
|
53
|
+
return "(advisor unavailable: no inference provider is configured)";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Append the consult instruction as the final user turn, then run a
|
|
57
|
+
// tool-less, capped completion through the resolved provider.
|
|
58
|
+
const messages: Message[] = [
|
|
59
|
+
...history,
|
|
60
|
+
userMessage(advisorRequestText(ADVISOR_CONFIG.wordLimit)),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const response = await provider.sendMessage(messages, {
|
|
64
|
+
systemPrompt: buildAdvisorSystem(params.systemPrompt),
|
|
65
|
+
config: {
|
|
66
|
+
callSite: ADVISOR_CALL_SITE,
|
|
67
|
+
overrideProfile: ADVISOR_CONFIG.profile,
|
|
68
|
+
tool_choice: { type: "none" },
|
|
69
|
+
max_tokens: ADVISOR_CONFIG.maxTokens,
|
|
70
|
+
},
|
|
71
|
+
signal: withTimeout(params.signal, ADVISOR_CONFIG.timeoutMs),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const advice = extractAllText(response).trim();
|
|
75
|
+
return advice.length > 0 ? advice : "(advisor returned no guidance)";
|
|
76
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `post-model-call` hook: snapshot the transcript the executor just saw. Fires
|
|
3
|
+
* after the model returns its message (which may carry the `advisor` tool_use)
|
|
4
|
+
* but before tools run, so when `advisor.execute()` runs an instant later it
|
|
5
|
+
* reads exactly this snapshot.
|
|
6
|
+
*
|
|
7
|
+
* Pure observer: never mutates `content` or the continue/stop `decision`. Gated
|
|
8
|
+
* to the user-facing reply, so background/subagent/compaction calls — and the
|
|
9
|
+
* advisor's own `inference`-call-site sub-call — are ignored.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PluginHookFn, PostModelCallContext } from "@vellumai/plugin-api";
|
|
13
|
+
|
|
14
|
+
import { recordMessages } from "../advisor-state-store.js";
|
|
15
|
+
|
|
16
|
+
const postModelCall: PluginHookFn<PostModelCallContext> = async (ctx) => {
|
|
17
|
+
if (ctx.callSite !== "mainAgent") return;
|
|
18
|
+
if (ctx.error) return;
|
|
19
|
+
// `ctx.messages` is the pre-reply history; the turn the model just produced —
|
|
20
|
+
// including any text/plan it wrote before the `advisor` tool_use — lives in
|
|
21
|
+
// `ctx.content` and is not yet in `messages`. Append it (cloned) so the
|
|
22
|
+
// advisor reviews the full current transcript, not just the prior history.
|
|
23
|
+
// `transcript.ts` strips the pending tool_use from this final assistant turn.
|
|
24
|
+
const messages =
|
|
25
|
+
ctx.content.length > 0
|
|
26
|
+
? [
|
|
27
|
+
...ctx.messages,
|
|
28
|
+
{ role: "assistant" as const, content: [...ctx.content] },
|
|
29
|
+
]
|
|
30
|
+
: ctx.messages;
|
|
31
|
+
recordMessages(ctx.conversationId, messages);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default postModelCall;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pre-model-call` hook: for the user-facing reply (`mainAgent`), capture the
|
|
3
|
+
* executor's system prompt (steering stripped) so the advisor can be given it
|
|
4
|
+
* as context, and inject the advisor steering so the model reaches for the
|
|
5
|
+
* tool. Idempotent via the steering marker.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PluginHookFn, PreModelCallContext } from "@vellumai/plugin-api";
|
|
9
|
+
|
|
10
|
+
import { recordSystemPrompt } from "../advisor-state-store.js";
|
|
11
|
+
import { ADVISOR_CONFIG } from "../config.js";
|
|
12
|
+
import { appendSteering, stripSteering } from "../steering.js";
|
|
13
|
+
|
|
14
|
+
const preModelCall: PluginHookFn<PreModelCallContext> = async (ctx) => {
|
|
15
|
+
if (ctx.callSite !== "mainAgent") return;
|
|
16
|
+
|
|
17
|
+
recordSystemPrompt(ctx.conversationId, stripSteering(ctx.systemPrompt));
|
|
18
|
+
|
|
19
|
+
if (ADVISOR_CONFIG.steeringEnabled) {
|
|
20
|
+
ctx.systemPrompt = appendSteering(ctx.systemPrompt);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default preModelCall;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `user-prompt-submit` hook: seed the per-conversation capture at the start of a
|
|
3
|
+
* user turn with the inbound history, so an advisor call on the very first model
|
|
4
|
+
* turn still has context even before `post-model-call` snapshots the running
|
|
5
|
+
* transcript.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
PluginHookFn,
|
|
10
|
+
UserPromptSubmitContext,
|
|
11
|
+
} from "@vellumai/plugin-api";
|
|
12
|
+
|
|
13
|
+
import { seedCapture } from "../advisor-state-store.js";
|
|
14
|
+
|
|
15
|
+
const userPromptSubmit: PluginHookFn<UserPromptSubmitContext> = async (ctx) => {
|
|
16
|
+
seedCapture(ctx.conversationId, ctx.latestMessages);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default userPromptSubmit;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "default-advisor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "First-party default plugin that adds an `advisor` tool: a no-argument tool the model calls mid-task to consult a stronger inference profile on the full conversation transcript for strategic guidance, routed through the assistant's own inference.",
|
|
5
|
+
"private": true,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.12.0"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@vellumai/plugin-api": "^0.8.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt fragments for the advisor plugin:
|
|
3
|
+
* - `appendSteering` / `stripSteering` — the executor-facing steering block the
|
|
4
|
+
* `pre-model-call` hook injects so the model reaches for the advisor tool at
|
|
5
|
+
* the right times.
|
|
6
|
+
* - `buildAdvisorSystem` / `advisorRequestText` — the advisor-facing framing for
|
|
7
|
+
* the consult itself.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Idempotency marker; the steering block is appended at the end of the prompt. */
|
|
11
|
+
export const STEERING_MARKER = "<!-- advisor:steering -->";
|
|
12
|
+
|
|
13
|
+
const STEERING_BODY = `You have an \`advisor\` tool backed by a stronger reviewer model. It takes NO parameters — calling it forwards your entire conversation automatically (the task, every tool call, every result). Call advisor BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. Orientation (reading files, fetching a source) is not substantive work; do that first, then call advisor. Also call it when stuck, when changing approach, and once before you declare the task done. Give its guidance serious weight; only override it when primary-source evidence contradicts a specific claim, and say so when you do.`;
|
|
14
|
+
|
|
15
|
+
const ADVISOR_STEERING = `${STEERING_MARKER}\n${STEERING_BODY}`;
|
|
16
|
+
|
|
17
|
+
/** Append the steering block to the executor's system prompt (idempotent). */
|
|
18
|
+
export function appendSteering(systemPrompt: string | null): string | null {
|
|
19
|
+
if (systemPrompt === null) return null;
|
|
20
|
+
if (systemPrompt.includes(STEERING_MARKER)) return systemPrompt;
|
|
21
|
+
return `${systemPrompt}\n\n${ADVISOR_STEERING}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Remove a previously-appended steering block, recovering the original prompt. */
|
|
25
|
+
export function stripSteering(systemPrompt: string | null): string | null {
|
|
26
|
+
if (systemPrompt === null) return null;
|
|
27
|
+
const idx = systemPrompt.indexOf(STEERING_MARKER);
|
|
28
|
+
if (idx === -1) return systemPrompt;
|
|
29
|
+
return systemPrompt.slice(0, idx).trimEnd();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* System prompt for the advisor sub-call. Frames the advisor's role and, for
|
|
34
|
+
* context, quotes the executor's own system prompt (as the advisor tool does —
|
|
35
|
+
* the advisor sees the system prompt as context about the executor's task).
|
|
36
|
+
*/
|
|
37
|
+
export function buildAdvisorSystem(
|
|
38
|
+
originalSystemPrompt: string | null,
|
|
39
|
+
): string {
|
|
40
|
+
const base = `You are a senior technical advisor consulted by another AI agent partway through a task. You can see the agent's full conversation above: its task, every tool call it has made, and every result it has seen. Give concise, high-leverage strategic guidance — the right approach, the key risk or failure mode to avoid, and the single most important next step. Do not role-play as the agent or produce its final deliverable; advise it. If the agent is already on track, say so briefly and sharpen its plan.`;
|
|
41
|
+
if (!originalSystemPrompt) return base;
|
|
42
|
+
return `${base}\n\nFor context, the agent is operating under this system prompt:\n<agent_system_prompt>\n${originalSystemPrompt}\n</agent_system_prompt>`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The final user turn appended to the transcript for the advisor sub-call. Asks
|
|
47
|
+
* for guidance and carries the soft word-limit nudge.
|
|
48
|
+
*/
|
|
49
|
+
export function advisorRequestText(wordLimit: number): string {
|
|
50
|
+
return `Review the conversation above — the task, the tool calls, and their results — and give focused strategic guidance on how to proceed. (Advisor: keep your guidance under ~${wordLimit} words — a focused starting point, not a comprehensive plan.)`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `advisor` tool — a no-argument tool the model calls to consult a stronger
|
|
3
|
+
* model for strategic guidance. The model supplies no input; the plugin reads
|
|
4
|
+
* the transcript captured by the lifecycle hooks and runs the consult, routed
|
|
5
|
+
* through the assistant's own inference.
|
|
6
|
+
*
|
|
7
|
+
* Default export = the tool definition. `defaults/index.ts` finalizes it and
|
|
8
|
+
* attaches it to the advisor plugin's `tools` array, which `bootstrapPlugins`
|
|
9
|
+
* registers into the model-visible tool catalog.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ToolContext,
|
|
14
|
+
ToolDefinition,
|
|
15
|
+
ToolExecutionResult,
|
|
16
|
+
} from "@vellumai/plugin-api";
|
|
17
|
+
import { RiskLevel } from "@vellumai/plugin-api";
|
|
18
|
+
|
|
19
|
+
import { getCapture } from "../advisor-state-store.js";
|
|
20
|
+
import { consultAdvisor } from "../consult.js";
|
|
21
|
+
|
|
22
|
+
const advisorTool: ToolDefinition = {
|
|
23
|
+
name: "advisor",
|
|
24
|
+
description:
|
|
25
|
+
"Consult a stronger advisor model for strategic guidance. Takes NO parameters — your " +
|
|
26
|
+
"full conversation (the task, every tool call, and every result) is forwarded " +
|
|
27
|
+
"automatically. Call it before substantive work, when you're stuck, when changing " +
|
|
28
|
+
"approach, and once before declaring a task complete.",
|
|
29
|
+
input_schema: { type: "object", properties: {}, additionalProperties: false },
|
|
30
|
+
// Read-only advice; low risk so the consult isn't gated behind a prompt.
|
|
31
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
32
|
+
async execute(
|
|
33
|
+
_input: Record<string, unknown>,
|
|
34
|
+
ctx: ToolContext,
|
|
35
|
+
): Promise<ToolExecutionResult> {
|
|
36
|
+
try {
|
|
37
|
+
const capture = getCapture(ctx.conversationId);
|
|
38
|
+
const advice = await consultAdvisor({
|
|
39
|
+
systemPrompt: capture?.systemPrompt ?? null,
|
|
40
|
+
messages: capture?.messages ?? [],
|
|
41
|
+
signal: ctx.signal,
|
|
42
|
+
});
|
|
43
|
+
return { content: advice, isError: false };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Degrade like the advisor tool: never fail the turn over a consult.
|
|
46
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
47
|
+
return { content: `(advisor unavailable: ${reason})`, isError: false };
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default advisorTool;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a captured executor transcript into the message list sent to the
|
|
3
|
+
* advisor sub-call.
|
|
4
|
+
*
|
|
5
|
+
* Strips blocks the advisor shouldn't (or can't) replay:
|
|
6
|
+
* - thinking / redacted-thinking (the advisor tool drops thinking),
|
|
7
|
+
* - files,
|
|
8
|
+
* - `server_tool_use` AND `web_search_tool_result` — provider-side tool calls
|
|
9
|
+
* (e.g. web search) and their results are dropped *together*. Dropping the
|
|
10
|
+
* result without its paired `server_tool_use` would leave an orphaned call
|
|
11
|
+
* block the provider rejects, so any consult after prior web-search history
|
|
12
|
+
* would fail; dropping both keeps the sequence valid.
|
|
13
|
+
*
|
|
14
|
+
* Images are preserved — top-level and nested inside `tool_result.contentBlocks`
|
|
15
|
+
* (which are recursively sanitized) — so the advisor sees what the executor saw.
|
|
16
|
+
* Visual tasks depend on it; the advisor profile is expected to be vision-capable.
|
|
17
|
+
*
|
|
18
|
+
* It also strips the *pending* client tool calls from the final assistant turn:
|
|
19
|
+
* at capture time (a `post-model-call` before tools run) the last assistant
|
|
20
|
+
* message carries the `advisor` tool_use with no matching `tool_result` yet, so
|
|
21
|
+
* sending it would be a dangling call. Earlier, completed `tool_use` /
|
|
22
|
+
* `tool_result` pairs are preserved intact.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ContentBlock, Message } from "../../../providers/types.js";
|
|
26
|
+
|
|
27
|
+
/** Drop disallowed blocks; recursively sanitize tool_result content. `null` = drop. */
|
|
28
|
+
function sanitize(block: ContentBlock): ContentBlock | null {
|
|
29
|
+
switch (block.type) {
|
|
30
|
+
case "thinking":
|
|
31
|
+
case "redacted_thinking":
|
|
32
|
+
case "file":
|
|
33
|
+
case "server_tool_use":
|
|
34
|
+
case "web_search_tool_result":
|
|
35
|
+
return null;
|
|
36
|
+
case "tool_result": {
|
|
37
|
+
if (!block.contentBlocks) return block;
|
|
38
|
+
// Keep images (and other allowed blocks) nested in the tool result; drop
|
|
39
|
+
// the disallowed ones.
|
|
40
|
+
const contentBlocks = block.contentBlocks
|
|
41
|
+
.map(sanitize)
|
|
42
|
+
.filter((b): b is ContentBlock => b !== null);
|
|
43
|
+
return {
|
|
44
|
+
...block,
|
|
45
|
+
contentBlocks: contentBlocks.length > 0 ? contentBlocks : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
// text, image, tool_use — kept.
|
|
50
|
+
return block;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function toAdvisorMessages(messages: ReadonlyArray<Message>): Message[] {
|
|
55
|
+
const out: Message[] = [];
|
|
56
|
+
const lastIndex = messages.length - 1;
|
|
57
|
+
|
|
58
|
+
messages.forEach((message, index) => {
|
|
59
|
+
let content = message.content
|
|
60
|
+
.map(sanitize)
|
|
61
|
+
.filter((b): b is ContentBlock => b !== null);
|
|
62
|
+
|
|
63
|
+
// The final assistant turn's client tool calls have no results yet — drop
|
|
64
|
+
// them so we never send a dangling tool_use. (server_tool_use is already
|
|
65
|
+
// dropped above.)
|
|
66
|
+
if (index === lastIndex && message.role === "assistant") {
|
|
67
|
+
content = content.filter(
|
|
68
|
+
(b) => b.type !== "tool_use" && b.type !== "server_tool_use",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (content.length > 0) out.push({ role: message.role, content });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
@@ -24,8 +24,15 @@
|
|
|
24
24
|
* {@link registerDefaultPlugins} at call time.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { finalizeTool } from "../../tools/tool-defaults.js";
|
|
27
28
|
import { registerPlugin, resetPluginRegistryForTests } from "../registry.js";
|
|
28
29
|
import { type Plugin, PluginExecutionError } from "../types.js";
|
|
30
|
+
import { resetAdvisorStateForTests } from "./advisor/advisor-state-store.js";
|
|
31
|
+
import advisorPostModelCall from "./advisor/hooks/post-model-call.js";
|
|
32
|
+
import advisorPreModelCall from "./advisor/hooks/pre-model-call.js";
|
|
33
|
+
import advisorUserPromptSubmit from "./advisor/hooks/user-prompt-submit.js";
|
|
34
|
+
import advisorPkg from "./advisor/package.json" with { type: "json" };
|
|
35
|
+
import advisorTool from "./advisor/tools/advisor.js";
|
|
29
36
|
import compactionPkg from "./compaction/package.json" with { type: "json" };
|
|
30
37
|
import emptyResponsePostModelCall from "./empty-response/hooks/post-model-call.js";
|
|
31
38
|
import emptyResponseStop from "./empty-response/hooks/stop.js";
|
|
@@ -297,6 +304,30 @@ export const defaultToolResultTruncatePlugin: Plugin = {
|
|
|
297
304
|
},
|
|
298
305
|
};
|
|
299
306
|
|
|
307
|
+
/**
|
|
308
|
+
* `advisor` — adds the model-visible `advisor` tool: a no-argument tool the
|
|
309
|
+
* model calls to consult a stronger inference profile (the `inference` call
|
|
310
|
+
* site with a `quality-optimized` override) on the full transcript, routed
|
|
311
|
+
* through the assistant's own inference. Three hooks feed it: `user-prompt-submit`
|
|
312
|
+
* seeds the capture, `pre-model-call` records the executor's system prompt and
|
|
313
|
+
* injects the steering that nudges the model to consult, and `post-model-call`
|
|
314
|
+
* snapshots the transcript the tool reads. `finalizeTool` fills the tool's
|
|
315
|
+
* defaults so it satisfies `Tool`, and `bootstrapPlugins` registers it into the
|
|
316
|
+
* catalog.
|
|
317
|
+
*/
|
|
318
|
+
export const defaultAdvisorPlugin: Plugin = {
|
|
319
|
+
manifest: {
|
|
320
|
+
name: advisorPkg.name,
|
|
321
|
+
version: advisorPkg.version,
|
|
322
|
+
},
|
|
323
|
+
hooks: {
|
|
324
|
+
"user-prompt-submit": advisorUserPromptSubmit,
|
|
325
|
+
"pre-model-call": advisorPreModelCall,
|
|
326
|
+
"post-model-call": advisorPostModelCall,
|
|
327
|
+
},
|
|
328
|
+
tools: [finalizeTool(advisorTool, "advisor")],
|
|
329
|
+
};
|
|
330
|
+
|
|
300
331
|
/**
|
|
301
332
|
* Full set of first-party default plugins. Used by
|
|
302
333
|
* {@link registerDefaultPlugins} to drive the registration loop; the array
|
|
@@ -318,6 +349,9 @@ function getAllDefaultPlugins(): readonly Plugin[] {
|
|
|
318
349
|
defaultCompactionPlugin,
|
|
319
350
|
defaultTitleGeneratePlugin,
|
|
320
351
|
memoryV3ShadowPlugin,
|
|
352
|
+
// Registered last so its capture hooks observe the fully-processed turn
|
|
353
|
+
// (memory injections, history repair) that the executor actually sees.
|
|
354
|
+
defaultAdvisorPlugin,
|
|
321
355
|
];
|
|
322
356
|
}
|
|
323
357
|
|
|
@@ -364,5 +398,6 @@ export function resetPluginRegistryAndRegisterDefaults(): void {
|
|
|
364
398
|
resetExplorationDriftStateForTests();
|
|
365
399
|
resetTaskProgressNudgeStateForTests();
|
|
366
400
|
resetSurfaceCompletionNudgeStoreForTests();
|
|
401
|
+
resetAdvisorStateForTests();
|
|
367
402
|
registerDefaultPlugins();
|
|
368
403
|
}
|
|
@@ -845,7 +845,7 @@ describe("Slack thinking status timing", () => {
|
|
|
845
845
|
channel: channelId,
|
|
846
846
|
threadTs,
|
|
847
847
|
status: expect.any(String),
|
|
848
|
-
loadingMessages: ["
|
|
848
|
+
loadingMessages: ["Thinking\u2026"],
|
|
849
849
|
},
|
|
850
850
|
);
|
|
851
851
|
const threadStatus = deliveredChannelReplies[0]!.payload
|
|
@@ -948,13 +948,13 @@ describe("Slack thinking status timing", () => {
|
|
|
948
948
|
channel: channelId,
|
|
949
949
|
threadTs,
|
|
950
950
|
status: expect.any(String),
|
|
951
|
-
loadingMessages: ["
|
|
951
|
+
loadingMessages: ["Thinking\u2026"],
|
|
952
952
|
},
|
|
953
953
|
{
|
|
954
954
|
channel: channelId,
|
|
955
955
|
threadTs,
|
|
956
956
|
status: expect.any(String),
|
|
957
|
-
loadingMessages: ["
|
|
957
|
+
loadingMessages: ["Thinking\u2026"],
|
|
958
958
|
},
|
|
959
959
|
{
|
|
960
960
|
channel: channelId,
|
|
@@ -577,7 +577,7 @@ function createSlackThinkingStatusController(params: {
|
|
|
577
577
|
}
|
|
578
578
|
|
|
579
579
|
const SLACK_THINKING_MAX_DURATION_MS = 120_000;
|
|
580
|
-
const SLACK_GENERIC_LOADING_MESSAGES = ["
|
|
580
|
+
const SLACK_GENERIC_LOADING_MESSAGES = ["Thinking…"] as const;
|
|
581
581
|
const SLACK_THINKING_STATUSES = ["is on it", "is working hard"] as const;
|
|
582
582
|
|
|
583
583
|
function getRandomSlackThinkingStatus(): string {
|