fix: offer no memory id for legacy manual memory, document linked history inputs

This commit is contained in:
hugocasa
2026-09-14 18:05:35 +02:00
parent 14218070e6
commit 3f7e3cdf26
14 changed files with 53 additions and 27 deletions
+3 -3
View File
@@ -453,9 +453,9 @@ pub async fn handle_ai_agent_job(
));
};
// A linked step takes its brain and tools from the resource and keeps only the flow-local
// inputs (user message, attachments, memory id and messages) of its own; both stay rigid, so the one thing it may
// bind to this flow is the tools' inputs, overlaid from `tool_inputs` below.
// A linked step takes its brain and tools from the resource and keeps only the flow-local inputs
// (user message, attachments, memory id and messages) of its own; both stay rigid, so the one
// thing it may bind to this flow is the tools' inputs, overlaid from `tool_inputs` below.
let (mut args, tools): (AIAgentArgs, Vec<AgentTool>) = if let Some(agent_ref) = agent.as_deref()
{
let agent_path = agent_ref
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -65,7 +65,8 @@ The worker reconciles them once per agent invocation, nested agent tools include
Memory is stored per (memory id, step id), in `ai_agent_memory` or S3 at
`memory/{workspace}/{memory id}/{step}.json`. The chat transcript (`flow_conversation_message`)
always follows the run's id, even when a step sets its own.
always follows the run's id, even when a step sets its own. Nothing expires stored memory: deleting a chat conversation deletes
its memory, and a memory named by a string id stays until it is overwritten.
Compatibility runs one way. New workers read every older shape. The editor rewrites a legacy step
only when the author changes it, so a flow nobody edits keeps running on older workers, while a
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ import {
AGENT_FIELD_BY_KEY,
AGENT_FIELDS,
AGENT_HISTORY_KEYS,
memoryIdUnusedNote,
agentFieldIsSet,
agentStreamingEnabled,
initialVisibleAgentFields
@@ -127,3 +128,16 @@ describe('agentStreamingEnabled', () => {
).toBe(false)
})
})
describe('memoryIdUnusedNote', () => {
// Mirrors the worker's order: offering a memory id that a run would ignore misleads the author.
it('offers a memory id only when the policy reads memory', () => {
expect(memoryIdUnusedNote(undefined)).toMatch(/off/)
expect(memoryIdUnusedNote({ kind: 'off' })).toMatch(/off/)
expect(memoryIdUnusedNote({ kind: 'window', context_length: 0 })).toMatch(/off/)
expect(memoryIdUnusedNote({ kind: 'auto' })).toMatch(/off/)
expect(memoryIdUnusedNote({ kind: 'manual', messages: [] })).toMatch(/fixed list/)
expect(memoryIdUnusedNote({ kind: 'window', context_length: 10 })).toBeUndefined()
expect(memoryIdUnusedNote({ kind: 'auto', context_length: 4, memory_id: 'x' })).toBeUndefined()
})
})
@@ -44,6 +44,19 @@ export function memoryPolicyIsOff(memory: any): boolean {
return false
}
/** Why a step's history row offers no memory id, when it offers none: the agent keeps no memory, or
* its memory setting supplies a fixed list of messages, which the worker sends before it would
* read any memory. */
export function memoryIdUnusedNote(memory: any): string | undefined {
if (memory?.kind === 'manual') {
return 'This agent sends a fixed list of messages set in its memory, so no memory id applies.'
}
if (memoryPolicyIsOff(memory)) {
return "This agent's memory is off, so no memory id applies. Turn memory on in the agent to change this."
}
return undefined
}
/** A memory setting in words, for a linked agent's summary. */
export function describeMemoryPolicy(memory: any): string {
if (memory?.kind === 'manual') return 'Provided messages'
@@ -121,7 +121,8 @@ export function inputTransformsToAgentConfig(
/**
* Reduce the AI agent schema to only the flow-local inputs. Used when a step is linked to a saved
* agent: the brain fields come from the resource, so only user_message/user_attachments stay editable.
* agent: the brain fields come from the resource, so only the user message, attachments and history
* inputs stay editable.
*/
export function flowLocalAgentSchema(schema: any): any {
if (!schema?.properties) {
@@ -14,8 +14,8 @@
label: string
tooltip?: string
chatInputEnabled?: boolean
/** The agent keeps no memory, so no memory id applies to this step. */
memoryOff?: boolean
/** Why no memory id applies to this step, when the agent reads no memory. */
memoryUnusedNote?: string
/** The step's editor for one history input, with the error to show under it. */
field: Snippet<[AgentHistoryKey, string | undefined]>
/** Called for each key this row removes, so the form forgets its validity. */
@@ -27,7 +27,7 @@
label,
tooltip = undefined,
chatInputEnabled = false,
memoryOff = false,
memoryUnusedNote = undefined,
field,
onRemoveKey = undefined
}: Props = $props()
@@ -58,11 +58,8 @@
<div class="flex min-h-7 items-end">
<FieldHeader {label} simpleTooltip={tooltip} displayType={false} />
</div>
{#if memoryOff}
<p class="text-xs text-secondary">
This agent's memory is off, so no memory id applies. Turn memory on in the agent to change
this.
</p>
{#if memoryUnusedNote}
<p class="text-xs text-secondary">{memoryUnusedNote}</p>
{#if source === 'here'}
<div class="flex items-center gap-2">
<Badge color="yellow" small>Ignored</Badge>
@@ -41,7 +41,7 @@
AGENT_FIELDS,
AGENT_HISTORY_ROW,
AGENT_TOOLS_ROW,
memoryPolicyIsOff,
memoryIdUnusedNote,
AGENT_FIELD_GROUPS,
agentFieldAppliesTo,
initialVisibleAgentFields,
@@ -164,14 +164,14 @@
// The brain edited here, or the linked agent's. An expression, or a linked agent still loading,
// reads as keeping memory, so the history row never offers less than a run may use.
let memoryOff = $derived.by(() => {
let memoryUnusedNote = $derived.by(() => {
if ('memory' in schemaProperties) {
const transform = args?.memory
return transform == undefined || transform.type === 'static'
? memoryPolicyIsOff(transform?.value)
: false
? memoryIdUnusedNote(transform?.value)
: undefined
}
return linkedMemory ? memoryPolicyIsOff(linkedMemory.memory) : false
return linkedMemory ? memoryIdUnusedNote(linkedMemory.memory) : undefined
})
let scopedFields = $derived(
@@ -415,7 +415,7 @@
label={spec.label}
tooltip={spec.tooltip}
{chatInputEnabled}
{memoryOff}
{memoryUnusedNote}
onRemoveKey={(key) => delete inputCheck[key]}
>
{#snippet field(key, error)}
+1 -1
View File
@@ -1155,7 +1155,7 @@ components:
Path of a reusable `ai_agent` resource (hybrid linking). When set, the agent brain
config (provider/model/system prompt/etc.) and tool set are resolved at runtime from
that resource; the module's input_transforms then only carry the flow-local inputs
(user_message/user_attachments).
(user_message, user_attachments and the history inputs memory_id and messages).
tool_inputs:
type: object
description: |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long