KV 与会话

workflow runtime 为每次执行注入 context.kv。KV 分三层:
await context.kv.setValue("global-key", "value");
await context.kv.workflow.setValue("workflow-key", { count: 1 });
await context.kv.conversation.setValue("conversation-key", true);
如果业务要求数据只能保存在 server 数据库中,使用 context.persistentValue。 它的三层 scope 与 context.kv 相同:
await context.persistentValue.setValue("global-key", "value");
await context.persistentValue.workflow.setValue("workflow-key", { count: 1 });
await context.persistentValue.conversation.setValue("conversation-key", true);

KV 作用域

  • context.kv:global scope。
  • context.kv.workflow:按 workflowName 隔离。
  • context.kv.conversation:按 workflowName + conversationId 隔离。
context.persistentValue 也提供 global / workflow / conversation 三层 scope。 conversation scope 仍然要求 executor 开启 conversation 并拥有非空 conversation id。 KV value 必须可 JSON 序列化,不能是 undefined。本地 CLI/App 的普通 context.kv 会落到本地 KV store;server 运行会落到 server storage。 context.persistentValue 是数据库版 KV。server 运行直接使用 server PostgreSQL store;CLI/App 本地运行会优先使用已配置的 WORKFLOW_SERVER_URL / WORKFLOW_SERVER_ADMIN_KEY,否则回退到 workspace CLI 保存的 workflow-auth.json,或使用 App 绑定的 server,把 PersistentValue 请求代理到 server 背后的数据库。只有没有连接服务器或 没有注入 persistent store 时,调用 context.persistentValue.*.getValue/setValue/updateValue 才会抛出明确错误。 无论在哪个平台执行,PersistentValue 都不会写入 CLI/App 本地 KV。 server PostgreSQL KV 会记录最后写入来源:last_workflow_namelast_run_idlast_conversation_id。这些字段用于 server/web 只读 KV 查看器把 key 关联回执行日志和 conversation。管理端 KV 工作台可按日志关联状态筛选,并展示关联覆盖率、payload 体积、scope 分布、value type 分布和热点 key。旧数据在字段上线前写入时不会自动回填来源,但仍能按 scope/key/value 展示。 server 会额外创建两个只读 view 方便接入开源工具:
  • workflow_project_kv_view:项目相关的 workflow / conversation KV。
  • workflow_global_kv_view:全局 global KV。
推荐用 NocoDB 连接这两个 view 做内部表格筛选,用 Metabase 或 Grafana 做 KV 数量、value type、最近写入趋势等图表。外部工具应使用只读数据库账号,不应直接写 workflow_kv_entries

项目知识库

issue #86 的第二阶段把 context.persistentValue 应用到项目内知识库。知识库文档固定包含 markdown 字段,并复用 workflow scope:
scopekey内容
workflowknowledge.documents文档索引、标题、摘要和更新时间。
workflowknowledge.document.<id>单篇 markdown 文档正文。
业务 workflow 可以通过 core helper 管理这些文档:
const document = await workflow.createKnowledgeDocument(context, {
  title: "Runbook",
  markdown: "# Runbook\n\nPersistent notes.",
});

const result = await workflow.searchKnowledgeDocuments(context, {
  query: "Persistent",
  documentId: document.id,
  beforeLines: 1,
  afterLines: 1,
});
可用方法包括 workflow.listKnowledgeDocumentsworkflow.getKnowledgeDocumentworkflow.createKnowledgeDocumentworkflow.editKnowledgeDocumentworkflow.deleteKnowledgeDocumentworkflow.searchKnowledgeDocumentsworkflow.readKnowledgeDocumentLines。这些方法只写 context.persistentValue.workflow;CLI/App 本地运行在已连接 server 时也可使用,未连接服务器持久存储时会按 PersistentValue 规则报错,不会把知识库落到本地 KV。删除文档会从索引移除并写入 tombstone 标记;当前 KV/PersistentValue 还没有 delete primitive。全文搜索支持 pageSize / cursor 分页和 beforeLines / afterLines 上下文行;指定文档的 n~m 行读取使用 readKnowledgeDocumentLines

Conversation 文档镜像

issue #86 的第三阶段增加了 workspace/workflow/conversation-knowledge 示例,用来把 conversation 应用作为知识库功能测试。这个 workflow 开启 executor.conversation.enabled,每次运行都会把用户消息和一个确定性的 assistant 确认写入 context.conversation,再把当前 conversation 的完整 transcript 转成 markdown 文档:
const state = await context.conversation.appendMessage({
  role: "assistant",
  content: `已记录到知识库文档: ${input.message}`,
});

const existing = await workflow.getKnowledgeDocument(context, documentId);
if (existing === undefined) {
  await workflow.createKnowledgeDocument(context, { id: documentId, title, markdown });
} else {
  await workflow.editKnowledgeDocument(context, { id: documentId, title, markdown });
}
文档 id 从 conversationId 派生,因此同一个 conversation 后续运行会更新同一篇知识库文档。示例提供 --dry-run,用于在没有 server PersistentValue 连接的本地测试中只预览将要写入的 markdown;真实写入可以通过已配置 server 的本地 CLI/App 运行,也可以通过 server API 运行,例如请求体传入 conversation_id 和业务 args

Token 用量统计

Token 用量统计默认关闭。executor 显式配置后,runtime 会把本次运行的 token 用量写入 KV,并把当前 run 聚合写入运行 report 和 runtime event:
export const executor = workflow.defineExecutor<Input, OutputPayload>({
  workflow: assistantWorkflow,
  tokenUsage: {
    enabled: true,
    display: true,
  },
  createInput({ args }) {
    return { message: args.join(" ") };
  },
});
enabled 控制统计与 KV 写入;display 控制 App/server 前端是否展示 token badge。display 省略时默认跟随 enabled 通过 workflow.getLLMProvider() 解析出的 provider 会自动读取 provider 返回的 usage;内置 LLM 节点和自定义节点直接调用 getLLMProvider() 都走同一条统计链路:
  • provider.chat() 在非流式输出中读取 output.usage
  • provider.stream() 在 stream finish chunk 中读取 usage,同一次 stream 只上报一次。
自定义节点可以手动上报:
await context.tokenUsage.report({
  inputTokens: 120,
  outputTokens: 80,
  totalTokens: 200,
  reasoningTokens: 12,
  cachedInputTokens: 40,
  cacheCreationTokens: 16,
  nodeName: "answer",
  providerName: "openai",
  modelId: "gpt-5",
});
未开启 tokenUsage.enabled 时,context.tokenUsage.report() 是安全 no-op。 cachedInputTokens 表示缓存命中的输入 token;cacheCreationTokens 表示本次调用写入/创建缓存的 token。App 和 server embed 的 badge 仍显示总量,例如 200 tokens;hover 或键盘 focus 时展示 Input、Output、Cache 三项明细,其中 Cache 为 cachedInputTokens + cacheCreationTokens 统计数据复用 KV 存储,不新增表:
scopekey内容
globaltokenUsage.total当前系统总 token 用量。
workflowtokenUsage.total当前 workflow/project 的总 token 用量;每个 workflow 只有一条总量记录。
conversationtokenUsage.total当前 conversation 的聚合 token 用量。
conversationtokenUsage.entry.<runId>.<sequence>每次上报的明细;一个 conversation 可以有多条。
KV namespace 提供 updateValue(key, updater),用于读改写聚合值。PostgreSQL KV 使用事务行锁减少并发丢增量;本地 KV store 使用读改写 fallback。

开启会话

会话能力默认关闭。必须在 executor 上显式开启:
export const executor = workflow.defineExecutor<Input, OutputPayload>({
  workflow: conversationWorkflow,
  conversation: {
    enabled: true,
  },
  createInput({ args, conversationId }) {
    return { message: args.join(" "), conversationId };
  },
});
开启后才能使用:
const state = await context.conversation.getValue();
await context.conversation.appendMessage({ role: "user", content: input.message });
await context.conversation.setValue("topic", "weather");
await context.conversation.setTitle("天气助手");
setTitle(title) 会把标题规范化后写入会话状态,并在 App/server embed 中触发标题更新事件。标题为空字符串时会清除标题;如果会话模式未开启,setTitle 是安全 no-op,不会因为标题只是 UI hint 而中断 workflow 执行。其它会话读写方法在未开启时仍会抛出明确错误。 如果 workflow 还声明了 conversation.defaultInput,App、server embed 和公开 embed 会优先渲染共享默认输入框,而不是把主消息继续当成普通 params 文本框。这个默认输入框固定序列化为三组 CLI 参数:
  • 文本:--message
  • 图片附件:--images
  • 普通文件附件:--files
workflow 仍需在 createInput({ args }) 中显式读取这些参数;推荐直接使用 workflow.parseConversationDefaultInputArgs(args)。默认输入框协议不会把主输入 自动塞进 WorkflowContext,它只约定宿主 UI 如何把消息和附件转换为稳定 args。 如果 workflow 同时声明了 conversation.inputQueue: { enabled: true },App 和 server embed 会在当前会话运行时继续接受新的输入,并把它们放入宿主持久化队列。 恢复后的队列默认处于等待状态:用户需要先手动发送一次新输入,之后等待中的队列项 才会自动按顺序继续执行。 App 和 server embed 还提供“锁定标题”开关。该锁定状态只存在于 Web/App UI 本地会话层,不属于 core API,workflow 代码不能设置锁定状态;锁定后 UI 会忽略 core setTitle 或最终 report 中的标题更新,手动解锁后才允许再次改名。未调用 setTitle 时,UI 会继续使用 conversation id 派生出的默认标题。

conversation_id

外部 API、server run、embed run 和 App 本地会话都可以传入或生成 conversation id。
  • App 会话面板为每个本地 chat session 生成并复用一个 conversation id。
  • 外部 API 使用请求体 conversation_id
  • Embed 会把请求的 conversation id 签名为 embed_<signature>_<nonce>,防止跨 token/session 复用。
  • 如果 webhook 或第三方 payload 需要先解析后才能确定稳定会话,createInput 可以返回非空的 conversationIdconversation_id。没有显式传入 conversation_id 时,runtime 会使用这个值作为执行级 conversation id;显式传入的 id 仍然优先。
  • Server 日志回放会优先使用最终输出里的 displayInput / display_input 作为 conversation 用户气泡展示内容,便于把 webhook payload 等原始输入转换成可读摘要;原始参数和节点详情仍保留用于排查。(issue #47

会话状态结构

runtime 内部保存:
interface WorkflowConversationState {
  conversationId: string;
  workflowName: string;
  title?: string;
  createdAt: string;
  updatedAt: string;
  messages: WorkflowConversationMessage[];
  values: Record<string, unknown>;
}
消息 role 支持 userassistantsystemtool

父子 workflow 会话复用

一个 workflow 可以通过 workflow.runWorkflow(childWorkflow, input, context, options) 复用另一个 workflow。子 workflow 默认复用父级 kvpersistentValueconversationtokenUsagefilesprovidersabortSignalnodeHooks,不会开启独立 run report。顶层 executor 入口不会额外插入 run-workflow 包裹节点,只有 workflow 内部显式调用另一个 workflow 时才会出现在父 trace 中。 默认 runWorkflow(child, input, context)runWorkflow(child, input, context, { name }) 都使用 conversationMode: "shared"outputVisibility: "visible"。给 trace 设置显示名不会静默改变会话写入或输出可见性。推荐桥接式父 workflow 调用子 workflow 时显式使用 conversationMode: "shared-readonly"。这种模式允许子 workflow 读取父会话快照,但 appendMessagesetValuesetTitle 不会写回父会话。父 workflow 应统一追加本轮 user/assistant transcript、设置标题,并决定最终可见 output,避免父子 workflow 对同一个 conversation 重复写消息。 conversationMode 只影响 context.conversation,不自动隔离 KV。默认 kvMode: "shared" 会让子 workflow 继续读写父级 workflow/conversation KV;如果被复用 workflow 会写入自己的 history、facts、cache 或其它 workflow-scoped 状态,推荐传入 kvMode: "isolated"。隔离模式会给 context.kv.workflowcontext.kv.conversation 的 key 增加以子 workflow 名称为基础的前缀,避免污染父 workflow 状态;context.kv global scope 仍然共享。context.persistentValue 是 server 持久通道,子 workflow 默认共享父级 persistentValue,不受 kvMode 隔离。 需要父级独占输出时,显式传入 outputVisibility: "hidden"。此时子 workflow 的 output 和 output stream chunks 不会进入父级可见输出,但子 workflow 的非输出节点事件仍进入父 trace。父 workflow 可以读取子 workflow 的返回值,再用自己的 stream output node 输出最终结果,避免父子 workflow 重复展示同一段内容。 如果父 workflow 的目标是复用并展示子 workflow 本身的输出,推荐在父级 output node 的 stream 中调用 runWorkflow,并传入 onOutputChunk / onOutput。子 workflow 支持流式 output 时,父级把 onOutputChunk 收到的 chunk 原样 yield;子 workflow 只有非流式 output 或直接返回 payload 时,父级把 onOutput 收到的结果转成当前 output item。这样父级仍能统一管理 conversation,但 UI 展示顺序由被调用 workflow 的真实输出顺序决定。 如果子 workflow 会展示工具调用过程,工具调用也应由子 workflow 自己作为流式 output item 产出,父级只负责转发。父级不要把工具事件重新组装到最终回答前后,否则容易出现空白等待或展示顺序反转。 runWorkflow 默认最多允许 16 层嵌套调用,防止 A 调 B、B 又调 A 这类环路无限递归。输出桥接 handler 抛错时,runtime 会让本次 runWorkflow 尽早失败,并在错误 metadata 中标记 outputBridgeFailed: true

外部 Agent SDK 会话

外部 agent SDK 不会自动理解 workflow-code 的 conversation/KV。示例 workspace/workflow/openai-agentsworkspace/workflow/claude-agent 展示了推荐映射方式:
  • 在 executor 上开启 conversation: { enabled: true }
  • 每轮运行前读取 context.conversation.getValue(),把最近消息、当前 conversation id 和 run count 作为 SDK run(..., { context }) 的纯 JSON 上下文。
  • SDK 工具如果需要持久化状态,写入 context.kv.conversation,例如 remember_fact{ key, value } 写成 conversation-scoped facts。
  • 自定义 function tool 可以直接编排 workflow 代码,例如示例中的 test_tool 会发起小型 HTTP GET/POST 请求并把响应摘要返回给 Agent;普通测试用本地 HTTP server 覆盖该网络请求路径。
  • 如果外部 SDK 需要访问本地上下文,示例可通过 --local-shell 启用 Agents SDK 的 shellTool 本地模式:environment: { type: "local" }。 实际 shell.run(...) 由 workflow 实现,并限制在 --workdir 内执行短命令。
  • SDK 返回后再调用 context.conversation.appendMessage(...) 记录用户和 assistant 消息,并把轻量 history 摘要写入 conversation KV。
  • 如果 SDK 自己有 session 概念,可以像 claude-agent 一样把 session id 写进 context.conversation state;同一个 workflow conversation 后续运行再把该 id 作为 SDK resume 参数传回去。
  • 外部 SDK 的 token usage 需要 workflow 手动调用 context.tokenUsage.report(...),这样 App/server badge 和 KV 统计才能 与内置 provider 保持一致。

追踪

本文档首版由 issue #32 记录。server/web KV 查看器与来源追踪由 issue #56 记录。PersistentValue 服务器持久值由 issue #86 记录。KV 与会话行为对齐 core/kv/index.tscore/conversation/index.tsserver/src/routes/embed.ts 和 App 会话运行逻辑。