-
Notifications
You must be signed in to change notification settings - Fork 0
feat(surface,sdk): f.memory via ai-hist — recall/why/learn (partial #307) #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import type { SlackCall } from './slack-writeback.js'; | |
| import { checkMcpHeader, McpPreflightError } from './cli/check-typescript.js'; | ||
| import { buildMcpProxy, runMcpEffect } from './authored-mcp.js'; | ||
| import { AuthoredBudget } from './authored-budget.js'; | ||
| import { assertMemoryReachable, authoredMemory, scriptMemoryScope } from './authored-memory.js'; | ||
| import { authoredWorkerRunner } from './authored-worker-step.js'; | ||
| import { readSuccessfulOutput, isSurfaceRunCompletionReason } from './authored-step-output.js'; | ||
| import { | ||
|
|
@@ -135,7 +136,7 @@ export async function executeAuthoredFlow<Input = undefined>( | |
| ...(options.onWait !== undefined ? { onWait: options.onWait } : {}), | ||
| }; | ||
| const definition = getDefinition<Input>(handle); | ||
| const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget'); | ||
| const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory'); | ||
| if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['slack', 'mcp'].includes(key))) headerFields.push('tools'); | ||
| if (definition.header.tools?.relayfile !== undefined) headerFields.push('tools.relayfile'); | ||
| const helperPreflight = checkSlackHelpers(definition); | ||
|
|
@@ -151,6 +152,15 @@ export async function executeAuthoredFlow<Input = undefined>( | |
| if (!checkedMcp.report.ok) throw new McpPreflightError(checkedMcp.report); | ||
|
|
||
| const budget = new AuthoredBudget(definition.header.budget); | ||
| if (definition.header.memory?.agent === true) { | ||
| throw new AuthoredFlowExecutionError('unsupported_header', 'memory.agent requires the follow-up identity-scoped agent memory adapter'); | ||
| } | ||
| // Direct member use is checked before any body effects; aliases are checked | ||
| // by the helper itself, without executing the body during discovery. | ||
| if (definition.header.memory !== undefined || /\.memory\b/.test(String(definition.body))) { | ||
| await assertMemoryReachable(); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memory preflight regex misclassifies flowsMedium Severity The eager memory probe decides reachability by testing Reviewed by Cursor Bugbot for commit 9fc682e. Configure here. |
||
|
|
||
| const journalSteps: AuthoredFlowJournalStep[] = []; | ||
| const authoredSteps: AuthoredFlowOperation<unknown>[] = []; | ||
| const lifecycle = new AuthoredFlowLifecycle(); | ||
|
|
@@ -231,6 +241,11 @@ export async function executeAuthoredFlow<Input = undefined>( | |
| lifecycle, | ||
| )); | ||
| }), | ||
| memory: authoredMemory( | ||
| scriptMemoryScope(flowPath, definition.name), | ||
| () => assertOperationAllowed('memory', definition.name, requestedCompletion), | ||
| definition.header.memory?.script !== false, | ||
| ), | ||
| run(command) { | ||
| assertOperationAllowed('run', definition.name, requestedCompletion); | ||
| const id = `run-${nextStep++}`; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import type { MemoryHelper } from '@relayflows/surface'; | ||
| import { createHash } from 'node:crypto'; | ||
| import { access, stat } from 'node:fs/promises'; | ||
| import { constants } from 'node:fs'; | ||
| import { dirname, join, resolve } from 'node:path'; | ||
| import { AuthoredFlowExecutionError } from './authored-flow-error.js'; | ||
| import { preflightMemory } from './preflight.js'; | ||
|
|
||
| /** Stable across runs; disjoint from other flow files and other named flows. */ | ||
| export function scriptMemoryScope(flowPath: string, name: string): string { | ||
| const absolute = resolve(flowPath); | ||
| const key = createHash('sha256').update(JSON.stringify([absolute, name])).digest('hex'); | ||
| return join(dirname(absolute), '.relayflows', 'memory', 'scripts', key); | ||
| } | ||
|
|
||
| export async function probeScriptMemory(): Promise<void> { | ||
| const { defaultDbPath, openAiHist } = await import('ai-hist'); | ||
| const path = defaultDbPath(); | ||
| await access(path, constants.R_OK); | ||
| if (!(await stat(path)).isFile()) throw new Error('memory database must be a file'); | ||
| const reader = await openAiHist({ dbPath: path, fallback: 'error' }); | ||
| try { reader.search('', { limit: 1 }); } finally { reader.close(); } | ||
| } | ||
|
|
||
| export async function assertMemoryReachable(): Promise<void> { | ||
| const refusal = await preflightMemory(probeScriptMemory); | ||
| if (refusal) throw new AuthoredFlowExecutionError('memory_unreachable', refusal.message); | ||
| } | ||
|
|
||
| export function authoredMemory( | ||
| scope: string, | ||
| assertOpen: () => void, | ||
| enabled: boolean, | ||
| ): MemoryHelper { | ||
| async function read<T>(action: (reader: import('ai-hist').AiHist) => T): Promise<T> { | ||
| assertOpen(); | ||
| if (!enabled) throw new AuthoredFlowExecutionError('unsupported_header', 'f.memory requires script memory; memory.script is false'); | ||
| await assertMemoryReachable(); | ||
| assertOpen(); | ||
| const { openAiHist } = await import('ai-hist'); | ||
| const reader = await openAiHist({ projectScope: scope, fallback: 'error' }); | ||
| try { return action(reader); } finally { reader.close(); } | ||
| } | ||
| return { | ||
| recall: (query, options) => read(reader => reader.search(query, { ...options, project: scope })), | ||
| why: task => read(reader => { | ||
| const entry = reader.whyForTask(task); | ||
| return entry?.projectId === scope ? [entry] : []; | ||
| }), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why() drops in-scope matchesHigh Severity
Reviewed by Cursor Bugbot for commit 1476610. Configure here. |
||
| async learn() { | ||
| assertOpen(); | ||
| throw new AuthoredFlowExecutionError('unsupported_verb', 'f.memory.learn requires the follow-up journal-backed trajectory writer'); | ||
| }, | ||
| }; | ||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.