mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 08:02:43 +00:00
feat: app conversations through the memory_id query parameter (#12025)
* feat: app conversations through the memory_id query parameter, chat-mode flows included Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: regenerate the mcp session instructions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: type backendAsync's memoryId option in the eval diagnostics Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: pin git-sync hub scripts to windmill-cli 1.849.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: memoryId option on the sync backend call too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: bump the ui builder artifact for the memoryId option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: shared-utils 1.0.14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: cli takes shared-utils 1.0.14 for the memoryId option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: pin the app conversation scoping and the memoryId signatures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: assert the ownership refusal, not any refusal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: match the memoryId option in the cli wmill.ts signature tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: file an app's turns as the deployed flow's conversation, preview included Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
003a6a9fd6
commit
07d4f7fe61
+2
-2
@@ -9,7 +9,7 @@
|
||||
"@cliffy/command": "npm:@jsr/cliffy__command@1.0.0",
|
||||
"@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0",
|
||||
"@cliffy/table": "npm:@jsr/cliffy__table@1.0.0",
|
||||
"@windmill-labs/shared-utils": "^1.0.13",
|
||||
"@windmill-labs/shared-utils": "^1.0.14",
|
||||
"diff": "^5.2.0",
|
||||
"esbuild": "0.28.2",
|
||||
"get-port": "7.1.0",
|
||||
@@ -175,7 +175,7 @@
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@windmill-labs/shared-utils": ["@windmill-labs/shared-utils@1.0.13", "", {}, "sha512-LB3bFq8i0bS2fwM8EqGRODrR1CXLXXyiNa766By1ZbSJYCmTzK8NVXqcluJP1ne7is80LRVZdOgWTw6YX+61Dg=="],
|
||||
"@windmill-labs/shared-utils": ["@windmill-labs/shared-utils@1.0.14", "", {}, "sha512-sD8G8EFQW+ml66kbsdvazDNOb96j9te88HHpwJDrJmNU5KXiuQuGkDo8beZgQ6SvDubY72pZjz5eJBIrbIXPWQ=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"@cliffy/command": "npm:@jsr/cliffy__command@1.0.0",
|
||||
"@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0",
|
||||
"@cliffy/table": "npm:@jsr/cliffy__table@1.0.0",
|
||||
"@windmill-labs/shared-utils": "^1.0.13",
|
||||
"@windmill-labs/shared-utils": "^1.0.14",
|
||||
"diff": "^5.2.0",
|
||||
"esbuild": "0.28.2",
|
||||
"get-port": "7.1.0",
|
||||
|
||||
+10
-11
@@ -1155,7 +1155,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
|
||||
colors.cyan(`[WebSocket] Received: ${JSON.stringify(message)}`),
|
||||
);
|
||||
|
||||
const { type, reqId, runnable_id, v, jobId } = message;
|
||||
const { type, reqId, runnable_id, v, jobId, memoryId } = message;
|
||||
|
||||
// Helper to send response
|
||||
const respond = (responseType: string, result: any, error: boolean) => {
|
||||
@@ -1166,6 +1166,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
|
||||
const runAndWaitForResult = async (
|
||||
runnableId: string,
|
||||
args: any,
|
||||
memoryId?: string,
|
||||
): Promise<{ uuid: string; result: any }> => {
|
||||
const runnables = await loadRunnables();
|
||||
const runnable = runnables[runnableId];
|
||||
@@ -1182,6 +1183,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
|
||||
args,
|
||||
appTempRefs,
|
||||
previewModules(runnables, runnableId),
|
||||
memoryId,
|
||||
);
|
||||
log.info(colors.gray(`[backend] Job started: ${uuid}`));
|
||||
|
||||
@@ -1194,7 +1196,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
|
||||
// Run a runnable synchronously and wait for result
|
||||
log.info(colors.blue(`[backend] Running runnable: ${runnable_id}`));
|
||||
try {
|
||||
const { result } = await runAndWaitForResult(runnable_id, v);
|
||||
const { result } = await runAndWaitForResult(runnable_id, v, memoryId);
|
||||
respond("backendRes", result, false);
|
||||
} catch (error: any) {
|
||||
log.error(colors.red(`[backend] Error: ${error.message}`));
|
||||
@@ -1230,6 +1232,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
|
||||
v,
|
||||
appTempRefs,
|
||||
previewModules(runnables, runnable_id),
|
||||
memoryId,
|
||||
);
|
||||
log.info(colors.gray(`[backendAsync] Job started: ${uuid}`));
|
||||
|
||||
@@ -1713,8 +1716,8 @@ export function buildWmillTs(
|
||||
|
||||
/**
|
||||
* The inputs of an agent runnable, after the agent's memory (see the frontend's
|
||||
* `agentRunnableSchema`): `memory_id` when it keeps managed memory, `previous_messages` when it
|
||||
* is off, and neither for an older `manual` setting.
|
||||
* `agentRunnableSchema`): `previous_messages` when it keeps no memory. The conversation of one
|
||||
* that does is `backendAsync`'s `memoryId` option, not an input.
|
||||
*/
|
||||
function agentRunnableSchema(memory: any) {
|
||||
const managed =
|
||||
@@ -1723,13 +1726,7 @@ function agentRunnableSchema(memory: any) {
|
||||
Boolean(memory.context_length));
|
||||
const legacy = memory?.kind === "manual";
|
||||
const history: Record<string, any> = {};
|
||||
if (managed) {
|
||||
history.memory_id = {
|
||||
type: "string",
|
||||
description:
|
||||
"The conversation the message belongs to: reuse it to continue one, or omit it for a single run",
|
||||
};
|
||||
} else if (!legacy) {
|
||||
if (!managed && !legacy) {
|
||||
history.previous_messages = {
|
||||
type: "array",
|
||||
description:
|
||||
@@ -1922,10 +1919,12 @@ async function executeRunnable(
|
||||
args: any,
|
||||
tempScriptRefs?: Record<string, string>,
|
||||
modules?: Record<string, { content: string; language: string }>,
|
||||
memoryId?: string,
|
||||
): Promise<string> {
|
||||
const requestBody: any = {
|
||||
component: runnableId,
|
||||
args: args ?? {},
|
||||
...(memoryId ? { run_query_params: { memory_id: memoryId } } : {}),
|
||||
force_viewer_static_fields: {},
|
||||
force_viewer_one_of_fields: {},
|
||||
force_viewer_allow_user_resources: [],
|
||||
|
||||
@@ -115,8 +115,8 @@ export const backend = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, runnable_id: string) {
|
||||
return (v: any) => {
|
||||
return doRequest('backend', { runnable_id, v })
|
||||
return (v: any, opts?: { memoryId?: string }) => {
|
||||
return doRequest('backend', { runnable_id, v, memoryId: opts?.memoryId })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -125,8 +125,8 @@ export const backendAsync = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, runnable_id: string) {
|
||||
return (v: any) => {
|
||||
return doRequest('backendAsync', { runnable_id, v })
|
||||
return (v: any, opts?: { memoryId?: string }) => {
|
||||
return doRequest('backendAsync', { runnable_id, v, memoryId: opts?.memoryId })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Generated
+2
-2
@@ -6882,7 +6882,7 @@ import * as wmill from './wmill'
|
||||
const chat = useWindmillChat({ wmill, runnable: 'chat' })
|
||||
\`\`\`
|
||||
|
||||
\`runnable\` is the runnable's key; each runnable keeps its own conversations. The runnable takes \`user_message\` and \`memory_id\`, the conversation id, which the chat fills in: the agent remembers earlier turns of a conversation, and a signed-in viewer's conversations are theirs alone. Called directly as \`backend.<key>({ user_message })\` without \`memory_id\`, it is a single run with no conversation, for a one-off question. The chat needs an agent with managed memory. An agent whose memory is off keeps none: its runnable takes \`previous_messages\` (chat messages) instead of \`memory_id\`, and the app keeps the history itself. The chat keeps its transcript in the app's own storage. \`chat.stop()\` stops following without cancelling the turn, and a message sent while it still runs is refused with \`TurnRunningError\`. Attachments are not supported this way.
|
||||
\`runnable\` is the runnable's key; each runnable keeps its own conversations. The runnable takes \`user_message\`; the chat passes the conversation id as \`backendAsync.<key>(args, { memoryId })\`: the agent remembers earlier turns of a conversation, and a signed-in viewer's conversations are theirs alone. Called directly as \`backend.<key>({ user_message })\`, it is a single run with no conversation, for a one-off question; \`backend.<key>(args, { memoryId })\` waits for the reply of a conversation turn. The chat needs an agent with managed memory. An agent whose memory is off keeps none: its runnable takes \`previous_messages\` (chat messages) instead, and the app keeps the history itself. The chat keeps its transcript in the app's own storage. \`chat.stop()\` stops following without cancelling the turn, and a message sent while it still runs is refused with \`TurnRunningError\`. Attachments are not supported this way.
|
||||
|
||||
## Backend runnables
|
||||
|
||||
@@ -6938,7 +6938,7 @@ Use only \`wmill\` functions the SDK actually exports; for an endpoint none of t
|
||||
|
||||
### Path runnables (script / flow / hubscript / agent)
|
||||
|
||||
When \`type\` is \`script\`, \`flow\`, \`hubscript\` or \`agent\`, the runnable just stores a \`path\` to an existing workspace or hub item — no inline code. The referenced item's input/output schema becomes the runnable's surface; an agent's is \`user_message\` and \`user_attachments\`, plus \`memory_id\` when the agent keeps managed memory or \`previous_messages\` when its memory is off.
|
||||
When \`type\` is \`script\`, \`flow\`, \`hubscript\` or \`agent\`, the runnable just stores a \`path\` to an existing workspace or hub item — no inline code. The referenced item's input/output schema becomes the runnable's surface; an agent's is \`user_message\` and \`user_attachments\`, plus \`previous_messages\` when its memory is off.
|
||||
|
||||
Before writing an inline runnable, look for a workspace script or flow that already does the job, or a Hub script (a prebuilt integration at \`hub/<version>/<app>/<name>\`) for a third-party service, and reference it instead of copying its logic.
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ test("fetchPathRunnableSchemas pulls schema for a path-based script and feeds wm
|
||||
expect(ts).toContain("userId");
|
||||
expect(ts).toContain("includeProfile");
|
||||
expect(ts).toMatch(/fetchUser:\s*\(args:\s*\{[^}]*userId/);
|
||||
expect(ts).not.toMatch(/fetchUser:\s*\(args:\s*\{\s*\}\)/);
|
||||
expect(ts).not.toMatch(/fetchUser:\s*\(args:\s*\{\s*\}[,)]/);
|
||||
|
||||
// Sanity: the generated d.ts is syntactically what the dev server writes
|
||||
const outPath = path.join(tempDir, "wmill.d.ts.out");
|
||||
|
||||
@@ -126,7 +126,7 @@ describe("genWmillTs (regression: path-based runnable schema)", () => {
|
||||
expect(ts).toMatch(/fetchUser:\s*\(args:\s*\{[^}]*userId/);
|
||||
expect(ts).toContain("userId");
|
||||
expect(ts).toContain("includeProfile");
|
||||
expect(ts).not.toMatch(/fetchUser:\s*\(args:\s*\{\s*\}\)/);
|
||||
expect(ts).not.toMatch(/fetchUser:\s*\(args:\s*\{\s*\}[,)]/);
|
||||
});
|
||||
|
||||
test("path-based runnable without schema falls back to {}", async () => {
|
||||
@@ -139,7 +139,7 @@ describe("genWmillTs (regression: path-based runnable schema)", () => {
|
||||
const runnables = await loadRunnablesFromBackend(backendDir);
|
||||
const ts = buildWmillTs(runnables);
|
||||
|
||||
expect(ts).toMatch(/noSchema:\s*\(args:\s*\{\s*\}\)/);
|
||||
expect(ts).toMatch(/noSchema:\s*\(args:\s*\{\s*\}[,)]/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -192,7 +192,7 @@ describe("inferRunnableSchemaFromFile (regression: new backend-folder format)",
|
||||
// and the runnable would render as `args: {}`.
|
||||
expect(ts).toContain("compute:");
|
||||
expect(ts).toMatch(/compute:\s*\(args:\s*\{[^}]*x:/);
|
||||
expect(ts).not.toMatch(/compute:\s*\(args:\s*\{\s*\}\)/);
|
||||
expect(ts).not.toMatch(/compute:\s*\(args:\s*\{\s*\}[,)]/);
|
||||
});
|
||||
|
||||
test("returns undefined for path-based runnables", async () => {
|
||||
@@ -297,6 +297,6 @@ describe("inferAllInlineSchemas (startup seed for wmill.d.ts)", () => {
|
||||
expect(ts).toMatch(/greet:\s*\(args:\s*\{[^}]*name/);
|
||||
expect(ts).toContain("name");
|
||||
expect(ts).toContain("times");
|
||||
expect(ts).not.toMatch(/greet:\s*\(args:\s*\{\s*\}\)/);
|
||||
expect(ts).not.toMatch(/greet:\s*\(args:\s*\{\s*\}[,)]/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user