88 taskWriteTool
99} from "@mastra/core/harness" ;
1010import { Mastra } from "@mastra/core/mastra" ;
11+ import { WorkingMemory } from "@mastra/core/processors" ;
1112import { createSkillTools , createWorkspaceTools } from "@mastra/core/workspace" ;
1213import type { Message } from "@ag-ui/core" ;
1314import type { ArtifactService } from "@datafoundry/artifacts" ;
@@ -47,6 +48,7 @@ import {
4748import {
4849 type TaskStateRuntime
4950} from "./memory/task-state-runtime.js" ;
51+ import { CONVERSATION_WORKING_MEMORY_CONFIG } from "./memory/conversation-memory-bridge.js" ;
5052import type { RuntimeContextSource } from "./context/source/runtime-context-source.js" ;
5153import { GoalRuntimeAdapter , type GoalRequest } from "./memory/goal-runtime-adapter.js" ;
5254import { createDataFoundryToolRegistry } from "./tools/data-tools.js" ;
@@ -131,10 +133,16 @@ export {
131133 resolveWorkspaceRoot
132134} from "./tools/workspace-factory.js" ;
133135export { resolvePythonRuntime } from "./tools/python-runtime.js" ;
134- export { projectWorkspaceObservation } from "./context/tool-observation/adapters/workspace-tool-observation-adapters.js" ;
136+ export {
137+ projectWorkspaceObservation
138+ } from "./context/tool-observation/adapters/workspace-tool-observation-adapters.js" ;
135139export { shouldReuseRecordedFileArtifactForPublish } from "./tools/artifact-publish-policy.js" ;
136140export { createDataFoundryToolRegistry , type ToolRegistry } from "./tools/data-tools.js" ;
137- export { GoalRuntimeAdapter , type GoalRequest , type GoalSnapshot } from "./memory/goal-runtime-adapter.js" ;
141+ export {
142+ GoalRuntimeAdapter ,
143+ type GoalRequest ,
144+ type GoalSnapshot
145+ } from "./memory/goal-runtime-adapter.js" ;
138146export { createContextItem , hashContextContent } from "./context/inventory/context-item.js" ;
139147export type {
140148 ContextItem ,
@@ -253,7 +261,7 @@ export const createDataFoundry = async (
253261 } ) ;
254262 }
255263 mkdirSync ( join ( skillCacheDir , "skills" ) , { recursive : true } ) ;
256- // 绑定到本次 session 的工作区:LocalFilesystem + LocalSandbox(macOS seatbelt / Linux bubblewrap 隔离) 。
264+ // 绑定到本次 session 的工作区:LocalFilesystem + LocalSandbox。
257265 // createDataFoundry 每次 run 都调用,直接闭包捕获 runContext,不依赖下游 requestContext 注入。
258266 const runWorkspace = createRunWorkspace ( {
259267 runContext : input . runContext ,
@@ -307,6 +315,9 @@ export const createDataFoundry = async (
307315 runState : contextRunState ,
308316 ...( input . taskStateRuntime ? { taskStateRuntime : input . taskStateRuntime } : { } )
309317 } ) ;
318+ const readOnlyWorkingMemoryProcessor = input . taskStateRuntime
319+ ? await createReadOnlyWorkingMemoryProcessor ( input . taskStateRuntime )
320+ : undefined ;
310321 const dashScopePromptCompat = new DashScopePromptCompatProcessor (
311322 shouldApplyDashScopePromptCompat ( input . modelProvider . kind ) ,
312323 ) ;
@@ -439,7 +450,11 @@ export const createDataFoundry = async (
439450 // Workspace remains attached for execution context, while auto-injection is disabled above.
440451 // Explicitly created tools are wrapped by the same governed execution boundary as every other tool.
441452 workspace : runWorkspace . workspace ,
442- inputProcessors : [ ...mastraContextProcessors . inputProcessors , dashScopePromptCompat ] ,
453+ inputProcessors : [
454+ ...( readOnlyWorkingMemoryProcessor ? [ readOnlyWorkingMemoryProcessor ] : [ ] ) ,
455+ ...mastraContextProcessors . inputProcessors ,
456+ dashScopePromptCompat
457+ ] ,
443458 outputProcessors : mastraContextProcessors . outputProcessors ,
444459 defaultOptions : {
445460 maxSteps : AGENT_MAX_STEPS ,
@@ -552,22 +567,34 @@ type MaterializedWorkspaceAttachment = {
552567const buildAgentInstructions = ( input : AgentInstructionsInput ) : string => {
553568 const { runContext : context , collaborationToolsEnabled, commandExecutionEnabled, taskToolsEnabled } = input ;
554569 const enabled = ( name : string ) : boolean => input . toolNames . includes ( name ) ;
570+ const publishArtifactEnabled = enabled ( "publish_artifact" ) ;
571+ const promoteWorkspaceFileEnabled = enabled ( "promote_workspace_file" ) ;
555572 const dataTools = [ "list_data_sources" , "inspect_schema" , "preview_table" , "run_sql_readonly" ] . filter ( enabled ) ;
556573 const toolGroups : string [ ] = dataTools . length > 0 ? [ `Data tools: ${ dataTools . join ( ", " ) } .` ] : [ ] ;
557574 if ( ( context . enabled_knowledge_ids ?. length ?? 0 ) > 0 && enabled ( "retrieve_knowledge" ) ) {
558575 toolGroups . push ( "Knowledge tools: retrieve_knowledge." ) ;
559576 }
560- if ( enabled ( "publish_artifact" ) ) {
577+ if ( publishArtifactEnabled ) {
561578 toolGroups . push ( "Artifact tools: publish_artifact." ) ;
562579 }
563580 const workspaceAssetTools = [ "list_workspace_files" , "read_workspace_file" , "promote_workspace_file" ]
564581 . filter ( enabled ) ;
565582 if ( workspaceAssetTools . length > 0 ) {
583+ const sessionProducerTools = [
584+ ...( enabled ( "write_file" ) ? [ "write_file" ] : [ ] ) ,
585+ ...( enabled ( "execute_command" ) ? [ "execute_command" ] : [ ] )
586+ ] ;
587+ const sessionProducerText = sessionProducerTools . length > 0
588+ ? `New files you write (${ sessionProducerTools . join ( " / " ) } ) are session-scoped — only this session sees them. `
589+ : "New files in the session workspace are session-scoped — only this session sees them. " ;
566590 toolGroups . push (
567591 `Workspace asset tools: ${ workspaceAssetTools . join ( ", " ) } . `
568- + "New files you write (write_file / execute_command) are session-scoped — only this session sees them. "
592+ + sessionProducerText
569593 + "list_workspace_files / read_workspace_file read the cross-session workspace root (shared across your "
570- + "sessions, read-only). promote_workspace_file copies a session file into that cross-session root."
594+ + "sessions, read-only). "
595+ + ( promoteWorkspaceFileEnabled
596+ ? "promote_workspace_file copies a session file into that cross-session root."
597+ : "Use only the available workspace asset tools listed above." )
571598 ) ;
572599 }
573600 const workspaceTools = [
@@ -584,18 +611,21 @@ const buildAgentInstructions = (input: AgentInstructionsInput): string => {
584611 toolGroups . push ( `Workspace tools (session-isolated directory): ${ workspaceTools . join ( ", " ) } . `
585612 + "Files you write stay within this session's workspace and can be reused by later runs in the same session. "
586613 + ( enabled ( "execute_command" )
587- ? ( input . pythonRuntimeAvailable
614+ ? ( input . pythonRuntimeAvailable
588615 ? "execute_command runs in a sandbox without network access. Use `python3.12 script.py` for local analysis; "
589- + "numpy, pandas, matplotlib, and scikit-learn are available from the project venv. Write scripts with write_file first, "
616+ + "numpy, pandas, matplotlib, and scikit-learn are available from the project venv. "
617+ + "Write scripts with write_file first, "
590618 + "save charts with plt.savefig(), and persist outputs as workspace files. "
591619 + "Do not use execute_command for external services or direct database access."
592620 : "execute_command runs in a sandbox without network access. Use it only for local transforms, charts, "
593621 + "or exports; never use it to access external services." )
594- : "execute_command is disabled this run; rely on the available data and file tools only." ) ) ;
622+ : "Command execution is disabled this run; rely on the available data and file tools only." ) ) ;
595623 }
596624 if ( input . workspaceAttachments . length > 0 ) {
597625 toolGroups . push ( `Uploaded workspace input files: ${ input . workspaceAttachments
598- . map ( ( file ) => `${ file . path } (file_id=${ file . file_id } , mime=${ file . mime_type ?? "unknown" } , size=${ file . size_bytes } )` )
626+ . map ( ( file ) =>
627+ `${ file . path } (file_id=${ file . file_id } , mime=${ file . mime_type ?? "unknown" } , size=${ file . size_bytes } )`
628+ )
599629 . join ( "; " ) } .`) ;
600630 }
601631 // R-019: per-run @ mentions — focus signal, not a narrowing. The agent is told which
@@ -677,18 +707,22 @@ const buildAgentInstructions = (input: AgentInstructionsInput): string => {
677707 input . selectedSkills . map ( ( skill ) => skill . name ) . join ( ", " )
678708 } .`
679709 : "" ;
710+ const skillScriptPolicy = enabled ( "execute_command" )
711+ ? " Scripts from skills may be executed only through approved workspace tools such as execute_command."
712+ : " Treat scripts from skills as reference material this run because command execution is unavailable." ;
680713 policies . push (
681714 "Use skills as task guidance, not as executable tools. skill_search may search the full shared skill cache; "
682715 + "a search result does not by itself mean the skill was selected for this run."
683716 + selectedSkillHint
684717 + " When the task matches an available skill, call "
685718 + "skill_search or skill to load its instructions, then use normal approved tools to act. "
686719 + "Use skill_read for references, scripts, or assets that belong to a relevant loaded skill. "
687- + "Scripts from skills may be executed only through approved workspace tools such as execute_command."
720+ + skillScriptPolicy
688721 ) ;
689722 }
690723 if ( enabled ( "inspect_schema" ) && ( enabled ( "run_sql_readonly" ) || enabled ( "preview_table" ) ) ) {
691- policies . push ( "Inspect before you query, then reuse the schema_id. inspect_schema returns a schema_id token that authorizes "
724+ policies . push ( "Inspect before you query, then reuse the schema_id. "
725+ + "inspect_schema returns a schema_id token that authorizes "
692726 + "run_sql_readonly and preview_table; pass it as their schema_id argument. The first SQL or preview against a "
693727 + "datasource must be preceded by an inspect_schema for it; without a valid schema_id the tools fail with "
694728 + "SCHEMA_REQUIRED. The token enforces inspect-before-query ordering within this run; Data Gateway remains the "
@@ -698,12 +732,15 @@ const buildAgentInstructions = (input: AgentInstructionsInput): string => {
698732 if ( enabled ( "run_sql_readonly" ) ) {
699733 policies . push ( "Write read-only SQL only. Generate SELECT or WITH queries and run them through run_sql_readonly. "
700734 + "Do not attempt writes, DDL, or multi-statement scripts through SQL"
701- + ". Never use execute_command or direct database clients to bypass Data Gateway."
735+ + ( enabled ( "execute_command" )
736+ ? ". Never use execute_command or direct database clients to bypass Data Gateway."
737+ : ". Never use direct database clients to bypass Data Gateway." )
702738 ) ;
703739 }
704740 policies . push (
705741 "Reply in the same natural language as the user's latest request. If the user mixes languages, use the dominant "
706- + "language from the request. Keep SQL, code, table names, column names, and other technical identifiers unchanged."
742+ + "language from the request. Keep SQL, code, table names, column names, and other technical identifiers "
743+ + "unchanged."
707744 ) ;
708745 policies . push (
709746 `Respect limits. This run allows at most ${ AGENT_MAX_STEPS } steps and `
@@ -712,21 +749,33 @@ const buildAgentInstructions = (input: AgentInstructionsInput): string => {
712749 + "Prefer one focused query per datasource before refining."
713750 ) ;
714751 if ( commandExecutionEnabled ) {
752+ const artifactPublishPolicy = publishArtifactEnabled
753+ ? "Call publish_artifact for files that should be exposed to the client as artifacts. "
754+ : "Do not claim a file was exposed as a client-visible artifact unless a tool result confirms it. " ;
755+ const workspacePromotionPolicy = promoteWorkspaceFileEnabled
756+ ? "Call promote_workspace_file only to lift a session workspace file into a cross-session reusable asset "
757+ + "(files in the same session are already retained across runs; do not promote merely to reuse within this "
758+ + "session)."
759+ : "" ;
715760 policies . push (
716761 "Persist derived artifacts in the workspace. When analysis produces exports, charts, or transformed datasets, "
717762 + "write them as files via write_file so they are retained with the session, rather than only echoing them in "
718- + "the final message. Call publish_artifact for files that should be exposed to the client as artifacts. "
763+ + "the final message. "
764+ + artifactPublishPolicy
719765 + "Do not invent download URLs, link text, or UI placement such as 'click the link below'; the client renders "
720766 + "download controls from artifact events and file APIs. "
721- + "Call promote_workspace_file only to lift a session workspace file into a cross-session reusable asset "
722- + "(files in the same session are already retained across runs; do not promote merely to reuse within this session)."
767+ + workspacePromotionPolicy
723768 ) ;
724769 if ( input . pythonRuntimeAvailable ) {
725770 policies . push (
726- "For Python analysis, prefer write_file to create a .py script, then execute_command with `python3.12 <script>`. "
771+ "For Python analysis, prefer write_file to create a .py script, then execute_command with "
772+ + "`python3.12 <script>`. "
727773 + "Use pandas for tabular work, matplotlib with plt.savefig() for charts (no GUI display), and scikit-learn "
728- + "for modeling. Export CSV/JSON/PNG artifacts to the session workspace and publish_artifact when the user "
729- + "should download results."
774+ + "for modeling. "
775+ + ( publishArtifactEnabled
776+ ? "Export CSV/JSON/PNG artifacts to the session workspace and publish_artifact when the user "
777+ + "should download results."
778+ : "Export CSV/JSON/PNG files to the session workspace when the user should reuse results." )
730779 ) ;
731780 }
732781 }
@@ -775,6 +824,25 @@ const selectToolsByPolicy = <TTool>(
775824 ) ) ;
776825} ;
777826
827+ const createReadOnlyWorkingMemoryProcessor = async (
828+ runtime : TaskStateRuntime
829+ ) : Promise < WorkingMemory | undefined > => {
830+ const memoryStore = await runtime . storage . getStore ( "memory" ) ;
831+ if ( ! memoryStore ) {
832+ return undefined ;
833+ }
834+ return new WorkingMemory ( {
835+ storage : memoryStore ,
836+ readOnly : true ,
837+ scope : CONVERSATION_WORKING_MEMORY_CONFIG . workingMemory . scope ,
838+ template : {
839+ format : "markdown" ,
840+ content : CONVERSATION_WORKING_MEMORY_CONFIG . workingMemory . template
841+ } ,
842+ templateProvider : runtime . memory
843+ } ) ;
844+ } ;
845+
778846const requireFileAssetService = ( service : FileAssetService | undefined ) : FileAssetService => {
779847 if ( ! service ) {
780848 throw new Error ( "SKILL_FILE_ASSET_SERVICE_REQUIRED" ) ;
@@ -986,7 +1054,14 @@ const throwIfAborted = (signal?: AbortSignal | undefined): void => {
9861054} ;
9871055
9881056/** List files under a directory (one level) relative to the workspace root, read-only. */
989- const listWorkspaceFiles = ( dirPath : string ) : Array < { path : string ; name : string ; size_bytes : number ; is_directory : boolean } > => {
1057+ type WorkspaceFileEntry = {
1058+ is_directory : boolean ;
1059+ name : string ;
1060+ path : string ;
1061+ size_bytes : number ;
1062+ } ;
1063+
1064+ const listWorkspaceFiles = ( dirPath : string ) : WorkspaceFileEntry [ ] => {
9901065 let entries : import ( "node:fs" ) . Dirent [ ] ;
9911066 try {
9921067 entries = readdirSync ( dirPath , { withFileTypes : true } ) ;
0 commit comments