Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 710d42d6b9 | |||
| 072d900fcb | |||
| cfebd07124 | |||
| f2be6fc057 | |||
| d392563549 | |||
| 2af8196451 | |||
| ad74768630 | |||
| a38ca7e8db | |||
| 3d97968887 | |||
| ade6227ffe | |||
| 13789e2c66 | |||
| 6758adc1d5 | |||
| 7c12015855 | |||
| 0f6859678c | |||
| 84798510b0 | |||
| 6eace09826 | |||
| cb39a6693a | |||
| 86dd37b0c8 | |||
| ec0bc672f6 | |||
| f08ba6914c | |||
| f6dd4d59a1 | |||
| d8cdc8ab88 | |||
| 20ddc5d7aa | |||
| 2846311f8d | |||
| ed0043b8ac | |||
| bee3911f3f | |||
| 4285b8b180 | |||
| f0b7be79fb | |||
| d4f05adeba |
@@ -9,3 +9,4 @@ bunfig.toml
|
|||||||
xiaoju/
|
xiaoju/
|
||||||
solve-issue-entry.ts
|
solve-issue-entry.ts
|
||||||
packages/workflow-template-develop/develop.esm.js
|
packages/workflow-template-develop/develop.esm.js
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
A workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file identified by its XXH64 hash (Crockford Base32). No daemon — processes start on demand and exit when done.
|
A workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file identified by its XXH64 hash (Crockford Base32). No daemon — processes start on demand and exit when done.
|
||||||
|
|
||||||
The implementation lives in **15** Bun workspace packages under `packages/`, using the `workspace:*` protocol.
|
The implementation lives in **21** Bun workspace packages under `packages/`, using the `workspace:*` protocol.
|
||||||
|
|
||||||
## Package map
|
## Package map
|
||||||
|
|
||||||
@@ -26,10 +26,13 @@ Grouped by responsibility (npm name → folder).
|
|||||||
| CLI | `@uncaged/cli-workflow` → `cli-workflow` | `uncaged-workflow` binary (depends on engine, registry, CAS, protocol, util, runtime). |
|
| CLI | `@uncaged/cli-workflow` → `cli-workflow` | `uncaged-workflow` binary (depends on engine, registry, CAS, protocol, util, runtime). |
|
||||||
| Agent adapters | `@uncaged/workflow-agent-cursor` → `workflow-agent-cursor` | `AgentFn` via `cursor-agent` CLI + workspace extraction. |
|
| Agent adapters | `@uncaged/workflow-agent-cursor` → `workflow-agent-cursor` | `AgentFn` via `cursor-agent` CLI + workspace extraction. |
|
||||||
| | `@uncaged/workflow-agent-hermes` → `workflow-agent-hermes` | `AgentFn` via `hermes chat` CLI. |
|
| | `@uncaged/workflow-agent-hermes` → `workflow-agent-hermes` | `AgentFn` via `hermes chat` CLI. |
|
||||||
|
| | `@uncaged/workflow-agent-office` → `workflow-agent-office` | `AdapterFn` via `office-agent` CLI; generates or edits Word documents, stores outputs per threadId. |
|
||||||
|
| | `@uncaged/workflow-agent-docx-diff` → `workflow-agent-docx-diff` | `AdapterFn` via `docx-diff` CLI; produces Word-format diff reports for document edit workflows. |
|
||||||
| | `@uncaged/workflow-agent-llm` → `workflow-agent-llm` | `AgentFn` via OpenAI-compatible HTTP (`LlmProvider` from runtime). |
|
| | `@uncaged/workflow-agent-llm` → `workflow-agent-llm` | `AgentFn` via OpenAI-compatible HTTP (`LlmProvider` from runtime). |
|
||||||
| Agent shared | `@uncaged/workflow-util-agent` → `workflow-util-agent` | `buildAgentPrompt`, `spawnCli` for CLI-backed agents. |
|
| Agent shared | `@uncaged/workflow-util-agent` → `workflow-util-agent` | `buildAgentPrompt`, `spawnCli` for CLI-backed agents. |
|
||||||
| Templates | `@uncaged/workflow-template-develop` → `workflow-template-develop` | Develop workflow definition, roles, descriptor builder. |
|
| Templates | `@uncaged/workflow-template-develop` → `workflow-template-develop` | Develop workflow definition, roles, descriptor builder. |
|
||||||
| | `@uncaged/workflow-template-solve-issue` → `workflow-template-solve-issue` | Solve-issue workflow definition, roles, descriptor builder. |
|
| | `@uncaged/workflow-template-solve-issue` → `workflow-template-solve-issue` | Solve-issue workflow definition, roles, descriptor builder. |
|
||||||
|
| | `@uncaged/workflow-template-document` → `workflow-template-document` | Document generation/editing workflow definition (writer + differ roles, moderator table, descriptor). |
|
||||||
| Dashboard | `@uncaged/workflow-dashboard` → `workflow-dashboard` | Private Vite + React app (`src/main.tsx`); only `react` / `react-dom` dependencies — no workspace packages. |
|
| Dashboard | `@uncaged/workflow-dashboard` → `workflow-dashboard` | Private Vite + React app (`src/main.tsx`); only `react` / `react-dom` dependencies — no workspace packages. |
|
||||||
|
|
||||||
## Dependency graph (workspace packages)
|
## Dependency graph (workspace packages)
|
||||||
@@ -265,4 +268,4 @@ Tags are 8-char Crockford Base32 (40-bit random), one per call site. `grep "4KNM
|
|||||||
| **Single-file ESM** | Hash = version, self-contained bundle |
|
| **Single-file ESM** | Hash = version, self-contained bundle |
|
||||||
| **No daemon** | OS handles process lifecycle |
|
| **No daemon** | OS handles process lifecycle |
|
||||||
| **Crockford Base32** | Filesystem-safe, readable, compact |
|
| **Crockford Base32** | Filesystem-safe, readable, compact |
|
||||||
| **15-package split** | Clear boundaries: protocol ↔ runtime author API ↔ util/CAS/register ↔ execute ↔ CLI ↔ agents/templates/UI |
|
| **21-package split** | Clear boundaries: protocol ↔ runtime author API ↔ util/CAS/register ↔ execute ↔ CLI ↔ agents/templates/UI |
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
|||||||
|
# 设计文档:office-agent 文档生成/编辑 Workflow 体系
|
||||||
|
|
||||||
|
**日期:** 2026-05-18
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
在 monorepo 中新增三个包,实现通过 `office-agent` CLI 生成或编辑 Word 文档的完整 workflow 体系。
|
||||||
|
|
||||||
|
| 包 | npm name | 职责 |
|
||||||
|
|---|---|---|
|
||||||
|
| `workflow-template-document` | `@uncaged/workflow-template-document` | 纯结构:角色定义、meta schema、调度表、descriptor |
|
||||||
|
| `workflow-agent-office` | `@uncaged/workflow-agent-office` | writer 角色执行器:调用 `office-agent` CLI |
|
||||||
|
| `workflow-agent-docx-diff` | `@uncaged/workflow-agent-docx-diff` | differ 角色执行器:调用 `docx-diff` CLI |
|
||||||
|
|
||||||
|
Template 只定义结构,不含执行逻辑。执行器与 template 解耦。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、`workflow-template-document`
|
||||||
|
|
||||||
|
### Thread 启动输入
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/types.ts
|
||||||
|
type DocumentStartInput = {
|
||||||
|
prompt: string; // 用户指令
|
||||||
|
inputDocx: string | null; // null = 生成模式;本机绝对路径 = 编辑模式
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
start.content 为 JSON `{ prompt, inputDocx }` 或纯文本(fallback:generate 模式,整段作为 prompt)。
|
||||||
|
|
||||||
|
### 角色与 Meta
|
||||||
|
|
||||||
|
`WriterMeta` 使用 discriminated union,在 schema 层区分两种模式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const writerMetaSchema = z.discriminatedUnion("mode", [
|
||||||
|
z.object({
|
||||||
|
mode: z.literal("generate"),
|
||||||
|
outputDocx: z.string(), // 生成产物绝对路径
|
||||||
|
sourceDocx: z.null(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
mode: z.literal("edit"),
|
||||||
|
outputDocx: z.string(), // 修改后产物:<outputDir>/modified.docx
|
||||||
|
sourceDocx: z.string(), // 原始副本:<outputDir>/original.docx
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
type WriterMeta = z.infer<typeof writerMetaSchema>;
|
||||||
|
|
||||||
|
// differ:仅编辑模式执行
|
||||||
|
const differMetaSchema = z.object({
|
||||||
|
sourceDocx: z.string(),
|
||||||
|
modifiedDocx: z.string(),
|
||||||
|
diffDocx: z.string(),
|
||||||
|
});
|
||||||
|
type DifferMeta = z.infer<typeof differMetaSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
两个角色的 `systemPrompt` 均为 `""`。
|
||||||
|
|
||||||
|
### 调度表
|
||||||
|
|
||||||
|
```
|
||||||
|
START → writer ──(mode = "edit")──→ differ → END
|
||||||
|
↘(mode = "generate")→ END
|
||||||
|
```
|
||||||
|
|
||||||
|
### 公开导出
|
||||||
|
|
||||||
|
template 导出两个对象供消费方使用:
|
||||||
|
|
||||||
|
- `documentWorkflowDefinition: WorkflowDefinition<DocumentMeta>` — 传入 `createWorkflow` 的 `def` 参数
|
||||||
|
- `buildDocumentDescriptor(): WorkflowDescriptor` — bundle 导出用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// bundle 侧用法
|
||||||
|
export const descriptor = buildDocumentDescriptor();
|
||||||
|
export const run = createWorkflow(documentWorkflowDefinition, { adapter, overrides });
|
||||||
|
```
|
||||||
|
|
||||||
|
### 包文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/workflow-template-document/
|
||||||
|
src/
|
||||||
|
types.ts # DocumentStartInput
|
||||||
|
roles/
|
||||||
|
writer.ts # writerMetaSchema, WriterMeta, writerRole
|
||||||
|
differ.ts # differMetaSchema, DifferMeta, differRole
|
||||||
|
index.ts
|
||||||
|
roles.ts # DocumentMeta, documentRoles
|
||||||
|
moderator.ts # writerIsEditMode condition + documentTable
|
||||||
|
definition.ts # documentWorkflowDefinition
|
||||||
|
descriptor.ts # buildDocumentDescriptor()
|
||||||
|
index.ts
|
||||||
|
__tests__/
|
||||||
|
moderator.test.ts
|
||||||
|
package.json
|
||||||
|
tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@uncaged/workflow-protocol": "workspace:^",
|
||||||
|
"@uncaged/workflow-runtime": "workspace:^",
|
||||||
|
"@uncaged/workflow-register": "workspace:^",
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、`workflow-agent-office`
|
||||||
|
|
||||||
|
### office-agent CLI 接口
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成模式:在 CWD 生成 output.docx
|
||||||
|
office-agent create "<prompt>" -o output.docx
|
||||||
|
|
||||||
|
# 编辑模式:在 CWD 对 modified.docx 进行修改(覆写)
|
||||||
|
office-agent edit modified.docx "<instruction>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- 两个命令均为阻塞调用(CLI 内部消费 SSE,退出即完成)
|
||||||
|
- 输出文件落到调用方设定的 CWD
|
||||||
|
- 退出码 0 = 成功,非零 = 失败
|
||||||
|
|
||||||
|
### 文件命名约定
|
||||||
|
|
||||||
|
| 模式 | 文件 | 路径 |
|
||||||
|
|---|---|---|
|
||||||
|
| generate | 输出 | `<outputDir>/output.docx` |
|
||||||
|
| edit | 原始副本(workflow-owned 快照) | `<outputDir>/original.docx` |
|
||||||
|
| edit | 修改后产物 | `<outputDir>/modified.docx` |
|
||||||
|
|
||||||
|
edit 模式先将 `inputDocx` 复制为 `original.docx`(不可变快照),再复制为 `modified.docx`,对 `modified.docx` 调用 CLI。agent 覆写 `modified.docx`,`original.docx` 保持不变。differ 对比这两个 workflow-owned 文件,不依赖用户原始路径。
|
||||||
|
|
||||||
|
### 执行流程
|
||||||
|
|
||||||
|
**生成模式(`inputDocx = null`):**
|
||||||
|
1. `mkdir -p <outputDir>`(`<config.outputDir>/<ctx.threadId>`)
|
||||||
|
2. `const command = config.command ?? "office-agent"`
|
||||||
|
3. `spawnCli(command, ["create", prompt, "-o", "output.docx"], { cwd: outputDir, timeoutMs })`
|
||||||
|
4. 验证 `outputDir/output.docx` 存在
|
||||||
|
5. 返回 `JSON.stringify({ mode: "generate", outputDocx, sourceDocx: null })`
|
||||||
|
|
||||||
|
**编辑模式(`inputDocx ≠ null`):**
|
||||||
|
1. `mkdir -p <outputDir>`
|
||||||
|
2. `copyFile(inputDocx, <outputDir>/original.docx)`
|
||||||
|
3. `copyFile(inputDocx, <outputDir>/modified.docx)`
|
||||||
|
4. `const command = config.command ?? "office-agent"`
|
||||||
|
5. `spawnCli(command, ["edit", "modified.docx", prompt], { cwd: outputDir, timeoutMs })`
|
||||||
|
6. 验证 `outputDir/modified.docx` 存在
|
||||||
|
7. 返回 `JSON.stringify({ mode: "edit", outputDocx: modifiedPath, sourceDocx: originalPath })`
|
||||||
|
|
||||||
|
### AdapterFn 实现(直接实现,不经过 runtime.extract)
|
||||||
|
|
||||||
|
CLI 产出确定性 JSON,直接 `schema.parse(JSON.parse(raw))` 跳过 LLM extraction:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function createOfficeAgent(config: OfficeAgentConfig): AdapterFn {
|
||||||
|
return <T>(_systemPrompt: string, schema: z.ZodType<T>) =>
|
||||||
|
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
||||||
|
const { prompt, inputDocx } = parseStartInput(ctx.start.content);
|
||||||
|
const raw = await runOfficeAgent(config, ctx.threadId, prompt, inputDocx);
|
||||||
|
const meta = schema.parse(JSON.parse(raw)) as T;
|
||||||
|
return { meta, childThread: null };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`_systemPrompt` 为 writer 角色的 systemPrompt(空字符串),实际指令从 `ctx.start.content` 解析。
|
||||||
|
|
||||||
|
### 配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type OfficeAgentConfig = {
|
||||||
|
outputDir: string; // 输出根目录,runner 在此下按 threadId 建子目录
|
||||||
|
command: string | null; // null → runner 内 resolve 为 "office-agent"
|
||||||
|
timeout: number | null; // null → 不设超时;单位 ms
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (!result.ok) {
|
||||||
|
const e = result.error;
|
||||||
|
if (e.kind === "non_zero_exit")
|
||||||
|
throw new Error(`office-agent failed (exit ${e.exitCode}): ${e.stderr}`);
|
||||||
|
if (e.kind === "timeout")
|
||||||
|
throw new Error("office-agent: timed out");
|
||||||
|
// "spawn_failed"
|
||||||
|
throw new Error(`office-agent: spawn failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
if (!existsSync(expectedPath))
|
||||||
|
throw new Error(`office-agent: output file not found: ${expectedPath}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### packageDescriptor
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/package-descriptor.ts
|
||||||
|
export const packageDescriptor: PackageDescriptor = {
|
||||||
|
name: "@uncaged/workflow-agent-office",
|
||||||
|
version: "0.1.0",
|
||||||
|
capabilities: ["office-agent-cli", "docx-generate", "docx-edit"],
|
||||||
|
configSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["outputDir"],
|
||||||
|
properties: {
|
||||||
|
outputDir: { type: "string", description: "Root directory for workflow outputs." },
|
||||||
|
command: { anyOf: [{ type: "string" }, { type: "null" }], description: "Path to office-agent CLI; null uses PATH." },
|
||||||
|
timeout: { anyOf: [{ type: "number" }, { type: "null" }], description: "Timeout in ms; null means no limit." },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 包文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/workflow-agent-office/
|
||||||
|
src/
|
||||||
|
types.ts # OfficeAgentConfig, OfficeAgentOpt
|
||||||
|
runner.ts # runOfficeAgent()(spawnCli 封装 + 文件验证)
|
||||||
|
agent.ts # createOfficeAgent(): AdapterFn
|
||||||
|
package-descriptor.ts # packageDescriptor
|
||||||
|
index.ts
|
||||||
|
__tests__/
|
||||||
|
runner.test.ts
|
||||||
|
agent.test.ts
|
||||||
|
package.json
|
||||||
|
tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@uncaged/workflow-protocol": "workspace:^",
|
||||||
|
"@uncaged/workflow-util": "workspace:^",
|
||||||
|
"@uncaged/workflow-util-agent": "workspace:^"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、`workflow-agent-docx-diff`
|
||||||
|
|
||||||
|
`differ` 角色专用执行器。从 `ctx.steps` 读取 `WriterMeta`,调用本地 `docx-diff` CLI。
|
||||||
|
|
||||||
|
### docx-diff 退出码约定
|
||||||
|
|
||||||
|
| 退出码 | 含义 | runner 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | 无差异 | 正常,验证 diffDocx 存在 |
|
||||||
|
| 1 | 有差异 | 正常(显式处理为成功),验证 diffDocx 存在 |
|
||||||
|
| 2+ | 错误 | throw |
|
||||||
|
|
||||||
|
runner 收到 `SpawnCliError { kind: "non_zero_exit", exitCode: 1 }` 时视为成功,验证文件后继续;`exitCode >= 2` 才 throw。
|
||||||
|
|
||||||
|
### 执行流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 从 ctx.steps 找到 writer 步骤,读取 WriterMeta
|
||||||
|
2. 验证 mode === "edit"(否则 throw)
|
||||||
|
3. diffDocx = join(dirname(writer.outputDocx), "diff.docx")
|
||||||
|
4. const command = config.command ?? "docx-diff"
|
||||||
|
5. spawnCli(command,
|
||||||
|
[writer.sourceDocx, writer.outputDocx, "--output", "docx", "--out-file", diffDocx],
|
||||||
|
{ cwd: null, timeoutMs: null })
|
||||||
|
exit 0 或 1 → 验证 diffDocx 存在
|
||||||
|
exit 2+ → throw
|
||||||
|
6. 返回 JSON.stringify({ sourceDocx, modifiedDocx: writer.outputDocx, diffDocx })
|
||||||
|
```
|
||||||
|
|
||||||
|
### AdapterFn 实现(直接实现,不经过 runtime.extract)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function createDocxDiffAgent(config: DocxDiffAgentConfig = { command: null }): AdapterFn {
|
||||||
|
return <T>(_prompt: string, schema: z.ZodType<T>) =>
|
||||||
|
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
||||||
|
const writerStep = ctx.steps.find(s => s.role === "writer");
|
||||||
|
if (!writerStep) throw new Error("differ: no writer step found");
|
||||||
|
const writerMeta = writerStep.meta as WriterMeta;
|
||||||
|
if (writerMeta.mode !== "edit")
|
||||||
|
throw new Error("differ: writer did not run in edit mode");
|
||||||
|
const raw = await runDocxDiff(config, writerMeta);
|
||||||
|
const meta = schema.parse(JSON.parse(raw)) as T;
|
||||||
|
return { meta, childThread: null };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type DocxDiffAgentConfig = {
|
||||||
|
command: string | null; // null → runner 内 resolve 为 "docx-diff"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### packageDescriptor
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const packageDescriptor: PackageDescriptor = {
|
||||||
|
name: "@uncaged/workflow-agent-docx-diff",
|
||||||
|
version: "0.1.0",
|
||||||
|
capabilities: ["docx-diff-cli", "docx-diff-report"],
|
||||||
|
configSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
command: { anyOf: [{ type: "string" }, { type: "null" }], description: "Path to docx-diff CLI; null uses PATH." },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 包文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/workflow-agent-docx-diff/
|
||||||
|
src/
|
||||||
|
types.ts # DocxDiffAgentConfig
|
||||||
|
runner.ts # runDocxDiff()(exit 1 处理 + 文件验证)
|
||||||
|
agent.ts # createDocxDiffAgent(): AdapterFn
|
||||||
|
package-descriptor.ts # packageDescriptor
|
||||||
|
index.ts
|
||||||
|
__tests__/
|
||||||
|
runner.test.ts
|
||||||
|
agent.test.ts
|
||||||
|
package.json
|
||||||
|
tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@uncaged/workflow-protocol": "workspace:^",
|
||||||
|
"@uncaged/workflow-util-agent": "workspace:^",
|
||||||
|
"@uncaged/workflow-template-document": "workspace:^"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、外部 bundle(外部 workspace 消费)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createOfficeAgent } from "@uncaged/workflow-agent-office";
|
||||||
|
import { createDocxDiffAgent } from "@uncaged/workflow-agent-docx-diff";
|
||||||
|
import {
|
||||||
|
buildDocumentDescriptor,
|
||||||
|
documentWorkflowDefinition,
|
||||||
|
} from "@uncaged/workflow-template-document";
|
||||||
|
import { createWorkflow } from "@uncaged/workflow-runtime";
|
||||||
|
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const outputDir = join(getDefaultWorkflowStorageRoot(), "outputs");
|
||||||
|
|
||||||
|
export const descriptor = buildDocumentDescriptor();
|
||||||
|
export const run = createWorkflow(documentWorkflowDefinition, {
|
||||||
|
adapter: createOfficeAgent({ outputDir, command: null, timeout: null }),
|
||||||
|
overrides: { differ: createDocxDiffAgent() },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 不在范围内
|
||||||
|
|
||||||
|
- 重试逻辑(失败直接 throw)
|
||||||
|
- office-agent server 的启停管理(假设 server 已在运行)
|
||||||
|
- docx-diff HTML/terminal 格式输出(仅 docx)
|
||||||
|
- 跨机器执行(`inputDocx` 须为本机有效绝对路径)
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
"uwf": "./src/cli.ts"
|
"uwf": "./src/cli.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/json-cas": "^0.1.3",
|
"@uncaged/json-cas": "^0.3.0",
|
||||||
"@uncaged/json-cas-fs": "^0.1.2",
|
"@uncaged/json-cas-fs": "^0.3.0",
|
||||||
"@uncaged/uwf-agent-kit": "workspace:^",
|
"@uncaged/uwf-agent-kit": "workspace:^",
|
||||||
"@uncaged/uwf-moderator": "workspace:^",
|
"@uncaged/uwf-moderator": "workspace:^",
|
||||||
"@uncaged/uwf-protocol": "workspace:^",
|
"@uncaged/uwf-protocol": "workspace:^",
|
||||||
|
|||||||
+15
-16
@@ -12,10 +12,10 @@ import {
|
|||||||
import { cmdWorkflowList, cmdWorkflowPut, cmdWorkflowShow } from "./commands/workflow.js";
|
import { cmdWorkflowList, cmdWorkflowPut, cmdWorkflowShow } from "./commands/workflow.js";
|
||||||
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
|
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
|
||||||
import {
|
import {
|
||||||
cmdCasCat,
|
|
||||||
cmdCasGet,
|
cmdCasGet,
|
||||||
cmdCasHas,
|
cmdCasHas,
|
||||||
cmdCasPut,
|
cmdCasPut,
|
||||||
|
cmdCasReindex,
|
||||||
cmdCasRefs,
|
cmdCasRefs,
|
||||||
cmdCasSchemaGet,
|
cmdCasSchemaGet,
|
||||||
cmdCasSchemaList,
|
cmdCasSchemaList,
|
||||||
@@ -185,24 +185,13 @@ const cas = program.command("cas").description("Content-addressable storage oper
|
|||||||
|
|
||||||
cas
|
cas
|
||||||
.command("get")
|
.command("get")
|
||||||
.description("Read a CAS node as JSON")
|
.description("Read a CAS node (type + payload; use --timestamp to include timestamp)")
|
||||||
.argument("<hash>", "CAS hash (13 char)")
|
.argument("<hash>", "CAS hash (13 char)")
|
||||||
.action((hash: string) => {
|
.option("--timestamp", "Include timestamp in output")
|
||||||
|
.action((hash: string, opts: { timestamp?: boolean }) => {
|
||||||
const storageRoot = resolveStorageRoot();
|
const storageRoot = resolveStorageRoot();
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
writeOutput(await cmdCasGet(storageRoot, hash));
|
writeOutput(await cmdCasGet(storageRoot, hash, opts));
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cas
|
|
||||||
.command("cat")
|
|
||||||
.description("Output a CAS node (--payload for payload only)")
|
|
||||||
.argument("<hash>", "CAS hash (13 char)")
|
|
||||||
.option("--payload", "Output only the payload")
|
|
||||||
.action((hash: string, opts: { payload?: boolean }) => {
|
|
||||||
const storageRoot = resolveStorageRoot();
|
|
||||||
runAction(async () => {
|
|
||||||
writeOutput(await cmdCasCat(storageRoot, hash, opts));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -251,6 +240,16 @@ cas
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cas
|
||||||
|
.command("reindex")
|
||||||
|
.description("Rebuild type index from all CAS nodes")
|
||||||
|
.action(() => {
|
||||||
|
const storageRoot = resolveStorageRoot();
|
||||||
|
runAction(async () => {
|
||||||
|
writeOutput(await cmdCasReindex(storageRoot));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const casSchema = cas.command("schema").description("CAS schema operations");
|
const casSchema = cas.command("schema").description("CAS schema operations");
|
||||||
|
|
||||||
casSchema
|
casSchema
|
||||||
|
|||||||
@@ -28,26 +28,18 @@ function readJsonArg(fileOrInline: string): unknown {
|
|||||||
export async function cmdCasGet(
|
export async function cmdCasGet(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
hash: string,
|
hash: string,
|
||||||
|
opts: { timestamp?: boolean },
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const store = openStore(storageRoot);
|
const store = openStore(storageRoot);
|
||||||
const node = store.get(hash);
|
const node = store.get(hash);
|
||||||
if (node === null) {
|
if (node === null) {
|
||||||
throw new Error(`Node not found: ${hash}`);
|
throw new Error(`Node not found: ${hash}`);
|
||||||
}
|
}
|
||||||
return node;
|
if (opts.timestamp) {
|
||||||
}
|
return node;
|
||||||
|
|
||||||
export async function cmdCasCat(
|
|
||||||
storageRoot: string,
|
|
||||||
hash: string,
|
|
||||||
opts: { payload?: boolean },
|
|
||||||
): Promise<unknown> {
|
|
||||||
const store = openStore(storageRoot);
|
|
||||||
const node = store.get(hash);
|
|
||||||
if (node === null) {
|
|
||||||
throw new Error(`Node not found: ${hash}`);
|
|
||||||
}
|
}
|
||||||
return opts.payload ? node.payload : node;
|
const { timestamp: _, ...rest } = node as Record<string, unknown>;
|
||||||
|
return rest;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cmdCasPut(
|
export async function cmdCasPut(
|
||||||
@@ -108,10 +100,10 @@ export async function cmdCasSchemaList(
|
|||||||
// Include meta-schema itself
|
// Include meta-schema itself
|
||||||
entries.push({ hash: metaHash, title: "(meta-schema)" });
|
entries.push({ hash: metaHash, title: "(meta-schema)" });
|
||||||
|
|
||||||
for (const hash of store.list()) {
|
for (const hash of store.listByType(metaHash)) {
|
||||||
if (hash === metaHash) continue;
|
if (hash === metaHash) continue;
|
||||||
const node = store.get(hash);
|
const node = store.get(hash);
|
||||||
if (node !== null && node.type === metaHash) {
|
if (node !== null) {
|
||||||
const schema = node.payload as JSONSchema;
|
const schema = node.payload as JSONSchema;
|
||||||
const title =
|
const title =
|
||||||
(schema.title as string | undefined) ??
|
(schema.title as string | undefined) ??
|
||||||
@@ -123,6 +115,17 @@ export async function cmdCasSchemaList(
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cmdCasReindex(
|
||||||
|
storageRoot: string,
|
||||||
|
): Promise<{ status: string }> {
|
||||||
|
const indexDir = join(storageRoot, "cas", "_index");
|
||||||
|
const { rmSync } = await import("node:fs");
|
||||||
|
rmSync(indexDir, { recursive: true, force: true });
|
||||||
|
// Re-open store to trigger migration rebuild
|
||||||
|
openStore(storageRoot);
|
||||||
|
return { status: "reindexed" };
|
||||||
|
}
|
||||||
|
|
||||||
export async function cmdCasSchemaGet(
|
export async function cmdCasSchemaGet(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
hash: string,
|
hash: string,
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { createMemoryStore, refs, validate, walk } from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
import {
|
||||||
|
computeDurationMs,
|
||||||
|
extractLastAssistantContent,
|
||||||
|
messageToTurnPayload,
|
||||||
|
parseSessionIdFromStdout,
|
||||||
|
storeHermesSessionDetail,
|
||||||
|
} from "../src/session-detail.js";
|
||||||
|
import type { HermesSessionJson, HermesSessionMessage } from "../src/types.js";
|
||||||
|
|
||||||
|
describe("parseSessionIdFromStdout", () => {
|
||||||
|
test("reads session_id from the last non-empty line", () => {
|
||||||
|
const stdout = "Done.\n\nsession_id: 20260518_223724_45ab80\n";
|
||||||
|
expect(parseSessionIdFromStdout(stdout)).toBe("20260518_223724_45ab80");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null when trailing line is not session_id", () => {
|
||||||
|
expect(parseSessionIdFromStdout("only assistant text\n")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("messageToTurnPayload", () => {
|
||||||
|
test("maps assistant tool_calls to toolCalls", () => {
|
||||||
|
const msg: HermesSessionMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
reasoning: null,
|
||||||
|
tool_calls: [{ function: { name: "read_file", arguments: '{"path":"x"}' } }],
|
||||||
|
};
|
||||||
|
const turn = messageToTurnPayload(msg, 0);
|
||||||
|
expect(turn).toEqual({
|
||||||
|
index: 0,
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
toolCalls: [{ name: "read_file", args: '{"path":"x"}' }],
|
||||||
|
reasoning: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips user messages", () => {
|
||||||
|
const msg: HermesSessionMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: "hi",
|
||||||
|
reasoning: null,
|
||||||
|
tool_calls: null,
|
||||||
|
};
|
||||||
|
expect(messageToTurnPayload(msg, 0)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractLastAssistantContent", () => {
|
||||||
|
test("returns the last non-empty assistant content", () => {
|
||||||
|
const messages: HermesSessionMessage[] = [
|
||||||
|
{ role: "assistant", content: "first", reasoning: null, tool_calls: null },
|
||||||
|
{ role: "tool", content: "tool output", reasoning: null, tool_calls: null },
|
||||||
|
{ role: "assistant", content: "", reasoning: null, tool_calls: null },
|
||||||
|
{ role: "assistant", content: "final answer", reasoning: null, tool_calls: null },
|
||||||
|
];
|
||||||
|
expect(extractLastAssistantContent(messages)).toBe("final answer");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computeDurationMs", () => {
|
||||||
|
test("computes elapsed time from session_start", () => {
|
||||||
|
const now = Date.parse("2026-05-18T13:32:59.028640Z");
|
||||||
|
const duration = computeDurationMs("2026-05-18T13:31:59.028640Z", now);
|
||||||
|
expect(duration).toBe(60_000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("storeHermesSessionDetail", () => {
|
||||||
|
test("stores hermes-detail root with cas_ref turns walkable", async () => {
|
||||||
|
const session: HermesSessionJson = {
|
||||||
|
session_id: "20260518_133159_6a84e8",
|
||||||
|
model: "claude-opus-4.6",
|
||||||
|
session_start: "2026-05-18T13:31:59.028640",
|
||||||
|
messages: [
|
||||||
|
{ role: "user", content: "task", reasoning: null, tool_calls: null },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
reasoning: "thinking",
|
||||||
|
tool_calls: [{ function: { name: "terminal", arguments: "{}" } }],
|
||||||
|
},
|
||||||
|
{ role: "tool", content: "ok", reasoning: null, tool_calls: null },
|
||||||
|
{ role: "assistant", content: "done", reasoning: null, tool_calls: null },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = createMemoryStore();
|
||||||
|
const now = Date.parse("2026-05-18T13:32:59.028640");
|
||||||
|
const { detailHash, output } = await storeHermesSessionDetail(store, session, now);
|
||||||
|
|
||||||
|
expect(output).toBe("done");
|
||||||
|
|
||||||
|
const detailNode = store.get(detailHash);
|
||||||
|
expect(detailNode).not.toBeNull();
|
||||||
|
if (detailNode === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(validate(store, detailNode)).toBe(true);
|
||||||
|
expect(detailNode.payload).toMatchObject({
|
||||||
|
sessionId: "20260518_133159_6a84e8",
|
||||||
|
model: "claude-opus-4.6",
|
||||||
|
duration: 60_000,
|
||||||
|
turnCount: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const turnRefs = refs(store, detailNode);
|
||||||
|
expect(turnRefs).toHaveLength(3);
|
||||||
|
|
||||||
|
const visited: string[] = [];
|
||||||
|
walk(store, detailHash, (hash) => visited.push(hash));
|
||||||
|
expect(visited).toContain(detailHash);
|
||||||
|
for (const turnHash of turnRefs) {
|
||||||
|
expect(visited).toContain(turnHash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@uncaged/json-cas": "^0.3.0",
|
||||||
"@uncaged/uwf-agent-kit": "workspace:^"
|
"@uncaged/uwf-agent-kit": "workspace:^"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
import { type AgentContext, createAgent } from "@uncaged/uwf-agent-kit";
|
import { type AgentContext, type AgentRunResult, createAgent } from "@uncaged/uwf-agent-kit";
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadHermesSession,
|
||||||
|
parseSessionIdFromStdout,
|
||||||
|
storeHermesRawOutput,
|
||||||
|
storeHermesSessionDetail,
|
||||||
|
} from "./session-detail.js";
|
||||||
|
|
||||||
const HERMES_COMMAND = "hermes";
|
const HERMES_COMMAND = "hermes";
|
||||||
const HERMES_MAX_TURNS = 90;
|
const HERMES_MAX_TURNS = 90;
|
||||||
|
|
||||||
function buildHistorySummary(history: AgentContext["history"]): string {
|
function buildHistorySummary(steps: AgentContext["steps"]): string {
|
||||||
if (history.length === 0) {
|
if (steps.length === 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: string[] = ["## Previous Steps"];
|
const lines: string[] = ["## Previous Steps"];
|
||||||
for (let i = 0; i < history.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
const step = history[i];
|
const step = steps[i];
|
||||||
if (step === undefined) {
|
if (step === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -26,8 +33,10 @@ function buildHistorySummary(history: AgentContext["history"]): string {
|
|||||||
|
|
||||||
/** Assemble system prompt, task, and prior step outputs for Hermes. */
|
/** Assemble system prompt, task, and prior step outputs for Hermes. */
|
||||||
export function buildHermesPrompt(ctx: AgentContext): string {
|
export function buildHermesPrompt(ctx: AgentContext): string {
|
||||||
const parts: string[] = [ctx.systemPrompt, "", "## Task", ctx.prompt];
|
const roleDef = ctx.workflow.roles[ctx.role];
|
||||||
const historyBlock = buildHistorySummary(ctx.history);
|
const systemPrompt = roleDef?.systemPrompt ?? "";
|
||||||
|
const parts: string[] = [systemPrompt, "", "## Task", ctx.start.prompt];
|
||||||
|
const historyBlock = buildHistorySummary(ctx.steps);
|
||||||
if (historyBlock !== "") {
|
if (historyBlock !== "") {
|
||||||
parts.push("", historyBlock);
|
parts.push("", historyBlock);
|
||||||
}
|
}
|
||||||
@@ -76,9 +85,22 @@ function spawnHermesChat(prompt: string): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runHermes(ctx: AgentContext): Promise<string> {
|
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
|
||||||
const fullPrompt = buildHermesPrompt(ctx);
|
const fullPrompt = buildHermesPrompt(ctx);
|
||||||
return spawnHermesChat(fullPrompt);
|
const rawOutput = await spawnHermesChat(fullPrompt);
|
||||||
|
const { store } = ctx;
|
||||||
|
|
||||||
|
const sessionId = parseSessionIdFromStdout(rawOutput);
|
||||||
|
if (sessionId !== null) {
|
||||||
|
const session = await loadHermesSession(sessionId);
|
||||||
|
if (session !== null) {
|
||||||
|
const { detailHash, output } = await storeHermesSessionDetail(store, session);
|
||||||
|
return { output, detailHash };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailHash = await storeHermesRawOutput(store, rawOutput);
|
||||||
|
return { output: rawOutput, detailHash };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Agent CLI factory: parses argv, runs Hermes, extracts output, writes StepNode. */
|
/** Agent CLI factory: parses argv, runs Hermes, extracts output, writes StepNode. */
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { JSONSchema } from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
const HERMES_TOOL_CALL_SCHEMA: JSONSchema = {
|
||||||
|
type: "object",
|
||||||
|
required: ["name", "args"],
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
args: { type: "string" },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HERMES_TURN_SCHEMA: JSONSchema = {
|
||||||
|
title: "hermes-turn",
|
||||||
|
type: "object",
|
||||||
|
required: ["index", "role", "content"],
|
||||||
|
properties: {
|
||||||
|
index: { type: "integer" },
|
||||||
|
role: { type: "string", enum: ["assistant", "tool"] },
|
||||||
|
content: { type: "string" },
|
||||||
|
toolCalls: {
|
||||||
|
anyOf: [{ type: "array", items: HERMES_TOOL_CALL_SCHEMA }, { type: "null" }],
|
||||||
|
},
|
||||||
|
reasoning: {
|
||||||
|
anyOf: [{ type: "string" }, { type: "null" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HERMES_DETAIL_SCHEMA: JSONSchema = {
|
||||||
|
title: "hermes-detail",
|
||||||
|
type: "object",
|
||||||
|
required: ["sessionId", "model", "duration", "turnCount", "turns"],
|
||||||
|
properties: {
|
||||||
|
sessionId: { type: "string" },
|
||||||
|
model: { type: "string" },
|
||||||
|
duration: { type: "integer" },
|
||||||
|
turnCount: { type: "integer" },
|
||||||
|
turns: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string", format: "cas_ref" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Fallback detail when Hermes session file is unavailable. */
|
||||||
|
export const HERMES_RAW_OUTPUT_SCHEMA: JSONSchema = {
|
||||||
|
title: "hermes-raw-output",
|
||||||
|
type: "object",
|
||||||
|
required: ["text"],
|
||||||
|
properties: {
|
||||||
|
text: { type: "string" },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { bootstrap, putSchema, type Store } from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
import { HERMES_DETAIL_SCHEMA, HERMES_RAW_OUTPUT_SCHEMA, HERMES_TURN_SCHEMA } from "./schemas.js";
|
||||||
|
import type {
|
||||||
|
HermesDetailPayload,
|
||||||
|
HermesSessionJson,
|
||||||
|
HermesSessionMessage,
|
||||||
|
HermesToolCall,
|
||||||
|
HermesTurnPayload,
|
||||||
|
HermesTurnRole,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
const SESSION_ID_LINE = /^session_id:\s*(\S+)\s*$/i;
|
||||||
|
|
||||||
|
export function getHermesSessionsDir(): string {
|
||||||
|
return join(homedir(), ".hermes", "sessions");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHermesSessionPath(sessionId: string): string {
|
||||||
|
return join(getHermesSessionsDir(), `session_${sessionId}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse `session_id: …` from the last non-empty line of Hermes stdout. */
|
||||||
|
export function parseSessionIdFromStdout(stdout: string): string | null {
|
||||||
|
const lines = stdout.split(/\r?\n/).map((line) => line.trim());
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line === undefined || line === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const match = SESSION_ID_LINE.exec(line);
|
||||||
|
if (match?.[1] !== undefined) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseToolCalls(raw: unknown): HermesSessionMessage["tool_calls"] {
|
||||||
|
if (!Array.isArray(raw) || raw.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const calls: NonNullable<HermesSessionMessage["tool_calls"]> = [];
|
||||||
|
for (const entry of raw) {
|
||||||
|
if (!isRecord(entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fn = entry.function;
|
||||||
|
if (!isRecord(fn)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = fn.name;
|
||||||
|
const args = fn.arguments;
|
||||||
|
if (typeof name !== "string" || typeof args !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
calls.push({ function: { name, arguments: args } });
|
||||||
|
}
|
||||||
|
return calls.length > 0 ? calls : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessage(raw: unknown): HermesSessionMessage | null {
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const role = raw.role;
|
||||||
|
if (role !== "assistant" && role !== "tool" && role !== "user") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const content = typeof raw.content === "string" ? raw.content : raw.content === null ? null : "";
|
||||||
|
const reasoning =
|
||||||
|
typeof raw.reasoning === "string"
|
||||||
|
? raw.reasoning
|
||||||
|
: raw.reasoning === null || raw.reasoning === undefined
|
||||||
|
? null
|
||||||
|
: null;
|
||||||
|
const tool_calls = parseToolCalls(raw.tool_calls);
|
||||||
|
return { role, content, reasoning, tool_calls };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSessionJson(raw: unknown): HermesSessionJson | null {
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const session_id = raw.session_id;
|
||||||
|
const model = raw.model;
|
||||||
|
const session_start = raw.session_start;
|
||||||
|
const messagesRaw = raw.messages;
|
||||||
|
if (
|
||||||
|
typeof session_id !== "string" ||
|
||||||
|
typeof model !== "string" ||
|
||||||
|
typeof session_start !== "string" ||
|
||||||
|
!Array.isArray(messagesRaw)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const messages: HermesSessionMessage[] = [];
|
||||||
|
for (const entry of messagesRaw) {
|
||||||
|
const msg = normalizeMessage(entry);
|
||||||
|
if (msg !== null) {
|
||||||
|
messages.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { session_id, model, session_start, messages };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadHermesSession(sessionId: string): Promise<HermesSessionJson | null> {
|
||||||
|
const path = getHermesSessionPath(sessionId);
|
||||||
|
try {
|
||||||
|
const text = await readFile(path, "utf8");
|
||||||
|
const raw = JSON.parse(text) as unknown;
|
||||||
|
return parseSessionJson(raw);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeDurationMs(sessionStart: string, nowMs: number = Date.now()): number {
|
||||||
|
const startMs = Date.parse(sessionStart);
|
||||||
|
if (Number.isNaN(startMs)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.max(0, nowMs - startMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSessionToolCalls(
|
||||||
|
toolCalls: HermesSessionMessage["tool_calls"],
|
||||||
|
): HermesToolCall[] | null {
|
||||||
|
if (toolCalls === null || toolCalls.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return toolCalls.map((call) => ({
|
||||||
|
name: call.function.name,
|
||||||
|
args: call.function.arguments,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messageToTurnPayload(
|
||||||
|
message: HermesSessionMessage,
|
||||||
|
index: number,
|
||||||
|
): HermesTurnPayload | null {
|
||||||
|
if (message.role !== "assistant" && message.role !== "tool") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const role = message.role as HermesTurnRole;
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
role,
|
||||||
|
content: message.content ?? "",
|
||||||
|
toolCalls: mapSessionToolCalls(message.tool_calls),
|
||||||
|
reasoning: message.reasoning,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Last assistant message with non-empty text content (walks backward). */
|
||||||
|
export function extractLastAssistantContent(messages: HermesSessionMessage[]): string {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i];
|
||||||
|
if (msg === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (msg.role === "assistant" && msg.content !== null && msg.content.trim() !== "") {
|
||||||
|
return msg.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
type HermesSchemaHashes = {
|
||||||
|
turn: string;
|
||||||
|
detail: string;
|
||||||
|
rawOutput: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function registerHermesSchemas(store: Store): Promise<HermesSchemaHashes> {
|
||||||
|
await bootstrap(store);
|
||||||
|
const [turn, detail, rawOutput] = await Promise.all([
|
||||||
|
putSchema(store, HERMES_TURN_SCHEMA),
|
||||||
|
putSchema(store, HERMES_DETAIL_SCHEMA),
|
||||||
|
putSchema(store, HERMES_RAW_OUTPUT_SCHEMA),
|
||||||
|
]);
|
||||||
|
return { turn, detail, rawOutput };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function storeHermesSessionDetail(
|
||||||
|
store: Store,
|
||||||
|
session: HermesSessionJson,
|
||||||
|
nowMs: number = Date.now(),
|
||||||
|
): Promise<{ detailHash: string; output: string }> {
|
||||||
|
const schemas = await registerHermesSchemas(store);
|
||||||
|
const turnHashes: string[] = [];
|
||||||
|
let turnIndex = 0;
|
||||||
|
|
||||||
|
for (const message of session.messages) {
|
||||||
|
const turn = messageToTurnPayload(message, turnIndex);
|
||||||
|
if (turn === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const hash = await store.put(schemas.turn, turn);
|
||||||
|
turnHashes.push(hash);
|
||||||
|
turnIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detail: HermesDetailPayload = {
|
||||||
|
sessionId: session.session_id,
|
||||||
|
model: session.model,
|
||||||
|
duration: computeDurationMs(session.session_start, nowMs),
|
||||||
|
turnCount: turnHashes.length,
|
||||||
|
turns: turnHashes,
|
||||||
|
};
|
||||||
|
const detailHash = await store.put(schemas.detail, detail);
|
||||||
|
const output = extractLastAssistantContent(session.messages);
|
||||||
|
return { detailHash, output };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function storeHermesRawOutput(store: Store, rawOutput: string): Promise<string> {
|
||||||
|
const schemas = await registerHermesSchemas(store);
|
||||||
|
return store.put(schemas.rawOutput, { text: rawOutput });
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
export type HermesTurnRole = "assistant" | "tool";
|
||||||
|
|
||||||
|
export type HermesToolCall = {
|
||||||
|
name: string;
|
||||||
|
args: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesTurnPayload = {
|
||||||
|
index: number;
|
||||||
|
role: HermesTurnRole;
|
||||||
|
content: string;
|
||||||
|
toolCalls: HermesToolCall[] | null;
|
||||||
|
reasoning: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesDetailPayload = {
|
||||||
|
sessionId: string;
|
||||||
|
model: string;
|
||||||
|
duration: number;
|
||||||
|
turnCount: number;
|
||||||
|
turns: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesSessionToolCall = {
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesSessionMessage = {
|
||||||
|
role: string;
|
||||||
|
content: string | null;
|
||||||
|
tool_calls: HermesSessionToolCall[] | null;
|
||||||
|
reasoning: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesSessionJson = {
|
||||||
|
session_id: string;
|
||||||
|
model: string;
|
||||||
|
session_start: string;
|
||||||
|
messages: HermesSessionMessage[];
|
||||||
|
};
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/json-cas": "^0.1.3",
|
"@uncaged/json-cas": "^0.3.0",
|
||||||
"@uncaged/json-cas-fs": "^0.1.2",
|
"@uncaged/json-cas-fs": "^0.3.0",
|
||||||
"@uncaged/uwf-protocol": "workspace:^",
|
"@uncaged/uwf-protocol": "workspace:^",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"yaml": "^2.8.4"
|
"yaml": "^2.8.4"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Store } from "@uncaged/json-cas";
|
||||||
import type {
|
import type {
|
||||||
CasRef,
|
CasRef,
|
||||||
StartNodePayload,
|
StartNodePayload,
|
||||||
@@ -6,6 +7,7 @@ import type {
|
|||||||
ThreadId,
|
ThreadId,
|
||||||
} from "@uncaged/uwf-protocol";
|
} from "@uncaged/uwf-protocol";
|
||||||
import { createAgentStore, loadThreadsIndex, resolveStorageRoot } from "./storage.js";
|
import { createAgentStore, loadThreadsIndex, resolveStorageRoot } from "./storage.js";
|
||||||
|
import type { AgentStore } from "./storage.js";
|
||||||
import type { AgentContext } from "./types.js";
|
import type { AgentContext } from "./types.js";
|
||||||
|
|
||||||
type ChainState = {
|
type ChainState = {
|
||||||
@@ -20,8 +22,8 @@ function fail(message: string): never {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function walkChain(
|
function walkChain(
|
||||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"],
|
store: Store,
|
||||||
schemas: Awaited<ReturnType<typeof createAgentStore>>["schemas"],
|
schemas: AgentStore["schemas"],
|
||||||
headHash: CasRef,
|
headHash: CasRef,
|
||||||
): ChainState {
|
): ChainState {
|
||||||
const headNode = store.get(headHash);
|
const headNode = store.get(headHash);
|
||||||
@@ -77,7 +79,7 @@ function walkChain(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function expandOutput(
|
function expandOutput(
|
||||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"],
|
store: Store,
|
||||||
outputRef: CasRef,
|
outputRef: CasRef,
|
||||||
): unknown {
|
): unknown {
|
||||||
const node = store.get(outputRef);
|
const node = store.get(outputRef);
|
||||||
@@ -88,7 +90,7 @@ function expandOutput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function buildHistory(
|
async function buildHistory(
|
||||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"],
|
store: Store,
|
||||||
stepsNewestFirst: StepNodePayload[],
|
stepsNewestFirst: StepNodePayload[],
|
||||||
): Promise<StepContext[]> {
|
): Promise<StepContext[]> {
|
||||||
const chronological = [...stepsNewestFirst].reverse();
|
const chronological = [...stepsNewestFirst].reverse();
|
||||||
@@ -105,8 +107,8 @@ async function buildHistory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkflow(
|
async function loadWorkflow(
|
||||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"],
|
store: Store,
|
||||||
schemas: Awaited<ReturnType<typeof createAgentStore>>["schemas"],
|
schemas: AgentStore["schemas"],
|
||||||
workflowRef: CasRef,
|
workflowRef: CasRef,
|
||||||
) {
|
) {
|
||||||
const node = store.get(workflowRef);
|
const node = store.get(workflowRef);
|
||||||
@@ -141,22 +143,22 @@ export async function buildContext(threadId: ThreadId, role: string): Promise<Ag
|
|||||||
fail(`unknown role "${role}" in workflow "${workflow.name}"`);
|
fail(`unknown role "${role}" in workflow "${workflow.name}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = await buildHistory(store, chain.stepsNewestFirst);
|
const steps = await buildHistory(store, chain.stepsNewestFirst);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
threadId,
|
threadId,
|
||||||
role,
|
role,
|
||||||
systemPrompt: roleDef.systemPrompt,
|
start: chain.start,
|
||||||
prompt: chain.start.prompt,
|
steps,
|
||||||
history,
|
|
||||||
workflow,
|
workflow,
|
||||||
|
store,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BuildContextMeta = {
|
export type BuildContextMeta = {
|
||||||
storageRoot: string;
|
storageRoot: string;
|
||||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"];
|
store: Store;
|
||||||
schemas: Awaited<ReturnType<typeof createAgentStore>>["schemas"];
|
schemas: AgentStore["schemas"];
|
||||||
headHash: CasRef;
|
headHash: CasRef;
|
||||||
chain: ChainState;
|
chain: ChainState;
|
||||||
};
|
};
|
||||||
@@ -185,15 +187,15 @@ export async function buildContextWithMeta(
|
|||||||
fail(`unknown role "${role}" in workflow "${workflow.name}"`);
|
fail(`unknown role "${role}" in workflow "${workflow.name}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = await buildHistory(store, chain.stepsNewestFirst);
|
const steps = await buildHistory(store, chain.stepsNewestFirst);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
threadId,
|
threadId,
|
||||||
role,
|
role,
|
||||||
systemPrompt: roleDef.systemPrompt,
|
start: chain.start,
|
||||||
prompt: chain.start.prompt,
|
steps,
|
||||||
history,
|
|
||||||
workflow,
|
workflow,
|
||||||
|
store,
|
||||||
meta: { storageRoot, store, schemas, headHash, chain },
|
meta: { storageRoot, store, schemas, headHash, chain },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export type { BuildContextMeta } from "./context.js";
|
export type { BuildContextMeta } from "./context.js";
|
||||||
export { buildContext, buildContextWithMeta } from "./context.js";
|
export { buildContext, buildContextWithMeta } from "./context.js";
|
||||||
export { getConfigPath, getEnvPath, loadWorkflowConfig } from "./storage.js";
|
|
||||||
export type { ExtractResult, ResolvedLlmProvider } from "./extract.js";
|
export type { ExtractResult, ResolvedLlmProvider } from "./extract.js";
|
||||||
export {
|
export {
|
||||||
extract,
|
extract,
|
||||||
@@ -8,4 +7,5 @@ export {
|
|||||||
resolveModel,
|
resolveModel,
|
||||||
} from "./extract.js";
|
} from "./extract.js";
|
||||||
export { createAgent } from "./run.js";
|
export { createAgent } from "./run.js";
|
||||||
export type { AgentContext, AgentOptions, AgentRunFn } from "./types.js";
|
export { getConfigPath, getEnvPath, loadWorkflowConfig } from "./storage.js";
|
||||||
|
export type { AgentContext, AgentOptions, AgentRunFn, AgentRunResult } from "./types.js";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { buildContextWithMeta } from "./context.js";
|
|||||||
import { extract } from "./extract.js";
|
import { extract } from "./extract.js";
|
||||||
import type { AgentStore } from "./storage.js";
|
import type { AgentStore } from "./storage.js";
|
||||||
import { getEnvPath, loadWorkflowConfig, resolveStorageRoot } from "./storage.js";
|
import { getEnvPath, loadWorkflowConfig, resolveStorageRoot } from "./storage.js";
|
||||||
import type { AgentContext, AgentOptions } from "./types.js";
|
import type { AgentContext, AgentOptions, AgentRunResult } from "./types.js";
|
||||||
|
|
||||||
function fail(message: string): never {
|
function fail(message: string): never {
|
||||||
process.stderr.write(`${message}\n`);
|
process.stderr.write(`${message}\n`);
|
||||||
@@ -65,7 +65,7 @@ async function writeStepNode(options: {
|
|||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAgent(options: AgentOptions, ctx: AgentContext): Promise<string> {
|
async function runAgent(options: AgentOptions, ctx: AgentContext): Promise<AgentRunResult> {
|
||||||
return runWithMessage("agent run failed", () => options.run(ctx));
|
return runWithMessage("agent run failed", () => options.run(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +85,11 @@ async function extractOutput(
|
|||||||
|
|
||||||
async function persistStep(options: {
|
async function persistStep(options: {
|
||||||
ctx: Awaited<ReturnType<typeof buildContextWithMeta>>;
|
ctx: Awaited<ReturnType<typeof buildContextWithMeta>>;
|
||||||
rawOutput: string;
|
|
||||||
outputHash: CasRef;
|
outputHash: CasRef;
|
||||||
|
detailHash: CasRef;
|
||||||
agentName: string;
|
agentName: string;
|
||||||
}): Promise<CasRef> {
|
}): Promise<CasRef> {
|
||||||
const { store, schemas, chain, headHash } = options.ctx.meta;
|
const { store, schemas, chain, headHash } = options.ctx.meta;
|
||||||
const detailHash = await store.put(null, options.rawOutput);
|
|
||||||
return writeStepNode({
|
return writeStepNode({
|
||||||
store,
|
store,
|
||||||
schemas,
|
schemas,
|
||||||
@@ -98,7 +97,7 @@ async function persistStep(options: {
|
|||||||
prevHash: chain.headIsStart ? null : headHash,
|
prevHash: chain.headIsStart ? null : headHash,
|
||||||
role: options.ctx.role,
|
role: options.ctx.role,
|
||||||
outputHash: options.outputHash,
|
outputHash: options.outputHash,
|
||||||
detailHash,
|
detailHash: options.detailHash,
|
||||||
agentName: options.agentName,
|
agentName: options.agentName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -121,12 +120,12 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
|
|||||||
fail(`unknown role: ${role}`);
|
fail(`unknown role: ${role}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawOutput = await runAgent(options, ctx);
|
const agentResult = await runAgent(options, ctx);
|
||||||
const outputHash = await extractOutput(rawOutput, roleDef.outputSchema, storageRoot);
|
const outputHash = await extractOutput(agentResult.output, roleDef.outputSchema, storageRoot);
|
||||||
const stepHash = await persistStep({
|
const stepHash = await persistStep({
|
||||||
ctx,
|
ctx,
|
||||||
rawOutput,
|
|
||||||
outputHash,
|
outputHash,
|
||||||
|
detailHash: agentResult.detailHash,
|
||||||
agentName: agentLabel(options.name),
|
agentName: agentLabel(options.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import type { Hash, Store } from "@uncaged/json-cas";
|
import type { Hash, Store } from "@uncaged/json-cas";
|
||||||
import { putSchema } from "@uncaged/json-cas";
|
import { putSchema } from "@uncaged/json-cas";
|
||||||
import {
|
import { START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA } from "@uncaged/uwf-protocol";
|
||||||
START_NODE_SCHEMA,
|
|
||||||
STEP_NODE_SCHEMA,
|
|
||||||
WORKFLOW_SCHEMA,
|
|
||||||
} from "@uncaged/uwf-protocol";
|
|
||||||
|
|
||||||
export type UwfAgentSchemaHashes = {
|
export type UwfAgentSchemaHashes = {
|
||||||
workflow: Hash;
|
workflow: Hash;
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import type { StepContext, ThreadId, WorkflowPayload } from "@uncaged/uwf-protocol";
|
import type { Store } from "@uncaged/json-cas";
|
||||||
|
import type { ModeratorContext, ThreadId, WorkflowPayload } from "@uncaged/uwf-protocol";
|
||||||
|
|
||||||
export type AgentContext = {
|
export type AgentContext = ModeratorContext & {
|
||||||
threadId: ThreadId;
|
threadId: ThreadId;
|
||||||
role: string;
|
role: string;
|
||||||
systemPrompt: string;
|
store: Store;
|
||||||
prompt: string;
|
|
||||||
history: StepContext[];
|
|
||||||
workflow: WorkflowPayload;
|
workflow: WorkflowPayload;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AgentRunFn = (ctx: AgentContext) => Promise<string>;
|
export type AgentRunResult = {
|
||||||
|
output: string;
|
||||||
|
detailHash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentRunFn = (ctx: AgentContext) => Promise<AgentRunResult>;
|
||||||
|
|
||||||
export type AgentOptions = {
|
export type AgentOptions = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/json-cas-fs": "^0.1.3"
|
"@uncaged/json-cas-fs": "^0.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { packageDescriptor } from "../src/package-descriptor.js";
|
||||||
|
import { createDocxDiffAgent } from "../src/agent.js";
|
||||||
|
|
||||||
|
describe("createDocxDiffAgent", () => {
|
||||||
|
test("returns an AdapterFn (function)", () => {
|
||||||
|
const agent = createDocxDiffAgent({ command: null });
|
||||||
|
expect(typeof agent).toBe("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("AdapterFn returns a RoleFn (function)", () => {
|
||||||
|
const agent = createDocxDiffAgent({ command: null });
|
||||||
|
const roleFn = agent("", expect.anything() as never);
|
||||||
|
expect(typeof roleFn).toBe("function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("packageDescriptor", () => {
|
||||||
|
test("has correct name", () => {
|
||||||
|
expect(packageDescriptor.name).toBe("@uncaged/workflow-agent-docx-diff");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
|
import { ok, err } from "@uncaged/workflow-util";
|
||||||
|
import type { SpawnCliConfig } from "@uncaged/workflow-util-agent";
|
||||||
|
import { runDocxDiff } from "../src/runner.js";
|
||||||
|
|
||||||
|
type MockSpawnResult = Awaited<ReturnType<typeof import("@uncaged/workflow-util-agent").spawnCli>>;
|
||||||
|
|
||||||
|
function makeSpawn(result: MockSpawnResult) {
|
||||||
|
return mock(async (_cmd: string, _args: string[], _opts: SpawnCliConfig) => result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tempDir(): string {
|
||||||
|
const dir = join(tmpdir(), `diff-test-${Date.now()}`);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("runDocxDiff", () => {
|
||||||
|
test("exit 0: success, returns DifferMeta JSON", async () => {
|
||||||
|
const dir = tempDir();
|
||||||
|
const sourceDocx = join(dir, "original.docx");
|
||||||
|
const modifiedDocx = join(dir, "modified.docx");
|
||||||
|
const diffDocx = join(dir, "diff.docx");
|
||||||
|
writeFileSync(sourceDocx, "");
|
||||||
|
writeFileSync(modifiedDocx, "");
|
||||||
|
|
||||||
|
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
|
||||||
|
// simulate docx-diff creating the diff file
|
||||||
|
writeFileSync(diffDocx, "");
|
||||||
|
|
||||||
|
const raw = await runDocxDiff(
|
||||||
|
{ command: "docx-diff" },
|
||||||
|
sourceDocx,
|
||||||
|
modifiedDocx,
|
||||||
|
diffDocx,
|
||||||
|
spawnFn,
|
||||||
|
);
|
||||||
|
const meta = JSON.parse(raw);
|
||||||
|
expect(meta.sourceDocx).toBe(sourceDocx);
|
||||||
|
expect(meta.modifiedDocx).toBe(modifiedDocx);
|
||||||
|
expect(meta.diffDocx).toBe(diffDocx);
|
||||||
|
|
||||||
|
expect(spawnFn.mock.calls[0][1]).toEqual([
|
||||||
|
sourceDocx,
|
||||||
|
modifiedDocx,
|
||||||
|
"--output",
|
||||||
|
"docx",
|
||||||
|
"--out-file",
|
||||||
|
diffDocx,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("exit 1 (changes found): treated as success", async () => {
|
||||||
|
const dir = tempDir();
|
||||||
|
const sourceDocx = join(dir, "s.docx");
|
||||||
|
const modifiedDocx = join(dir, "m.docx");
|
||||||
|
const diffDocx = join(dir, "diff.docx");
|
||||||
|
writeFileSync(sourceDocx, "");
|
||||||
|
writeFileSync(modifiedDocx, "");
|
||||||
|
writeFileSync(diffDocx, "");
|
||||||
|
|
||||||
|
const spawnFn = makeSpawn(
|
||||||
|
err({ kind: "non_zero_exit", exitCode: 1, stdout: "", stderr: "" }) as MockSpawnResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runDocxDiff({ command: "docx-diff" }, sourceDocx, modifiedDocx, diffDocx, spawnFn),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("exit 2: throws error", async () => {
|
||||||
|
const dir = tempDir();
|
||||||
|
const spawnFn = makeSpawn(
|
||||||
|
err({ kind: "non_zero_exit", exitCode: 2, stdout: "", stderr: "fatal error" }) as MockSpawnResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runDocxDiff({ command: null }, "s.docx", "m.docx", "diff.docx", spawnFn),
|
||||||
|
).rejects.toThrow("docx-diff failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("timeout: throws error", async () => {
|
||||||
|
const spawnFn = makeSpawn(err({ kind: "timeout" }) as MockSpawnResult);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runDocxDiff({ command: null }, "s.docx", "m.docx", "diff.docx", spawnFn),
|
||||||
|
).rejects.toThrow("timed out");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when diff file not created", async () => {
|
||||||
|
const dir = tempDir();
|
||||||
|
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
|
||||||
|
// do NOT create diffDocx
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runDocxDiff({ command: null }, "s.docx", "m.docx", join(dir, "missing.docx"), spawnFn),
|
||||||
|
).rejects.toThrow("diff file not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses PATH docx-diff when command is null", async () => {
|
||||||
|
const dir = tempDir();
|
||||||
|
const diffDocx = join(dir, "diff.docx");
|
||||||
|
writeFileSync(diffDocx, "");
|
||||||
|
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
|
||||||
|
|
||||||
|
await runDocxDiff({ command: null }, "s.docx", "m.docx", diffDocx, spawnFn);
|
||||||
|
|
||||||
|
expect(spawnFn.mock.calls[0][0]).toBe("docx-diff");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@uncaged/workflow-agent-docx-diff",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"files": ["src", "dist", "package.json"],
|
||||||
|
"type": "module",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"bun": "./src/index.ts",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/workflow-runtime": "workspace:^",
|
||||||
|
"@uncaged/workflow-util-agent": "workspace:^",
|
||||||
|
"@uncaged/workflow-template-document": "workspace:^",
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@uncaged/workflow-util": "workspace:^"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import * as z from "zod/v4";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import type { AdapterFn, RoleResult, ThreadContext, WorkflowRuntime } from "@uncaged/workflow-runtime";
|
||||||
|
import type { WriterMeta } from "@uncaged/workflow-template-document";
|
||||||
|
import { runDocxDiff } from "./runner.js";
|
||||||
|
import type { DocxDiffAgentConfig } from "./types.js";
|
||||||
|
|
||||||
|
export function createDocxDiffAgent(config: DocxDiffAgentConfig): AdapterFn {
|
||||||
|
return <T>(_prompt: string, schema: z.ZodType<T>) =>
|
||||||
|
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
||||||
|
const writerStep = ctx.steps.find((s) => s.role === "writer");
|
||||||
|
if (writerStep === undefined) throw new Error("differ: no writer step found");
|
||||||
|
|
||||||
|
const writerMeta = writerStep.meta as WriterMeta;
|
||||||
|
if (writerMeta.mode !== "edit")
|
||||||
|
throw new Error("differ: writer did not run in edit mode");
|
||||||
|
|
||||||
|
const diffDocx = join(dirname(writerMeta.outputDocx), "diff.docx");
|
||||||
|
const raw = await runDocxDiff(
|
||||||
|
config,
|
||||||
|
writerMeta.sourceDocx,
|
||||||
|
writerMeta.outputDocx,
|
||||||
|
diffDocx,
|
||||||
|
);
|
||||||
|
|
||||||
|
const meta = schema.parse(JSON.parse(raw)) as T;
|
||||||
|
return { meta, childThread: null };
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { createDocxDiffAgent } from "./agent.js";
|
||||||
|
export { packageDescriptor } from "./package-descriptor.js";
|
||||||
|
export type { DocxDiffAgentConfig } from "./types.js";
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { PackageDescriptor } from "@uncaged/workflow-runtime";
|
||||||
|
|
||||||
|
export const packageDescriptor: PackageDescriptor = {
|
||||||
|
name: "@uncaged/workflow-agent-docx-diff",
|
||||||
|
version: "0.1.0",
|
||||||
|
capabilities: ["docx-diff-cli", "docx-diff-report"],
|
||||||
|
configSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
command: {
|
||||||
|
anyOf: [{ type: "string" }, { type: "null" }],
|
||||||
|
description: "Path to docx-diff CLI binary; null uses PATH.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { stat } from "node:fs/promises";
|
||||||
|
import { spawnCli } from "@uncaged/workflow-util-agent";
|
||||||
|
import type { SpawnCliError } from "@uncaged/workflow-util-agent";
|
||||||
|
import type { DocxDiffAgentConfig } from "./types.js";
|
||||||
|
|
||||||
|
type SpawnCliFn = typeof spawnCli;
|
||||||
|
|
||||||
|
function throwSpawnError(e: SpawnCliError): never {
|
||||||
|
if (e.kind === "non_zero_exit")
|
||||||
|
throw new Error(`docx-diff failed (exit ${e.exitCode}): ${e.stderr}`);
|
||||||
|
if (e.kind === "timeout")
|
||||||
|
throw new Error("docx-diff: timed out");
|
||||||
|
throw new Error(`docx-diff: spawn failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runDocxDiff(
|
||||||
|
config: DocxDiffAgentConfig,
|
||||||
|
sourceDocx: string,
|
||||||
|
modifiedDocx: string,
|
||||||
|
diffDocx: string,
|
||||||
|
spawnCliFn: SpawnCliFn = spawnCli,
|
||||||
|
): Promise<string> {
|
||||||
|
const command = config.command ?? "docx-diff";
|
||||||
|
const result = await spawnCliFn(
|
||||||
|
command,
|
||||||
|
[sourceDocx, modifiedDocx, "--output", "docx", "--out-file", diffDocx],
|
||||||
|
{ cwd: null, timeoutMs: null },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const e = result.error;
|
||||||
|
// exit 1 = changes found (normal for docx-diff)
|
||||||
|
if (e.kind === "non_zero_exit" && e.exitCode === 1) {
|
||||||
|
// fall through to file check
|
||||||
|
} else {
|
||||||
|
throwSpawnError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stat(diffDocx);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`docx-diff: diff file not found: ${diffDocx}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({ sourceDocx, modifiedDocx, diffDocx });
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type DocxDiffAgentConfig = {
|
||||||
|
command: string | null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../workflow-protocol" },
|
||||||
|
{ "path": "../workflow-runtime" },
|
||||||
|
{ "path": "../workflow-util-agent" },
|
||||||
|
{ "path": "../workflow-template-document" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { packageDescriptor } from "../src/package-descriptor.js";
|
||||||
|
import { createOfficeAgent } from "../src/agent.js";
|
||||||
|
|
||||||
|
describe("createOfficeAgent", () => {
|
||||||
|
test("returns an AdapterFn (function)", () => {
|
||||||
|
const agent = createOfficeAgent({ outputDir: "/tmp", command: null, timeout: null });
|
||||||
|
expect(typeof agent).toBe("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("AdapterFn returns a RoleFn (function)", () => {
|
||||||
|
const agent = createOfficeAgent({ outputDir: "/tmp", command: null, timeout: null });
|
||||||
|
const roleFn = agent("", expect.anything() as never);
|
||||||
|
expect(typeof roleFn).toBe("function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("packageDescriptor", () => {
|
||||||
|
test("has correct name", () => {
|
||||||
|
expect(packageDescriptor.name).toBe("@uncaged/workflow-agent-office");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has outputDir in configSchema required", () => {
|
||||||
|
const schema = packageDescriptor.configSchema as { required: string[] };
|
||||||
|
expect(schema.required).toContain("outputDir");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
|
import { ok, err } from "@uncaged/workflow-util";
|
||||||
|
import type { SpawnCliConfig } from "@uncaged/workflow-util-agent";
|
||||||
|
import { editDocument, generateDocument } from "../src/runner.js";
|
||||||
|
|
||||||
|
type MockSpawnResult = Awaited<ReturnType<typeof import("@uncaged/workflow-util-agent").spawnCli>>;
|
||||||
|
|
||||||
|
function makeSpawn(result: MockSpawnResult) {
|
||||||
|
return mock(async (_cmd: string, _args: string[], _opts: SpawnCliConfig) => result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tempDir(): string {
|
||||||
|
const dir = join(tmpdir(), `office-test-${Date.now()}`);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("generateDocument", () => {
|
||||||
|
test("calls office-agent create with correct args and returns outputDocx path", async () => {
|
||||||
|
const base = tempDir();
|
||||||
|
const spawnFn = makeSpawn(ok("agent reply") as MockSpawnResult);
|
||||||
|
// Simulate CLI creating the file
|
||||||
|
const outFile = join(base, "thread1", "output.docx");
|
||||||
|
mkdirSync(join(base, "thread1"), { recursive: true });
|
||||||
|
writeFileSync(outFile, "");
|
||||||
|
|
||||||
|
const result = await generateDocument(
|
||||||
|
{ outputDir: base, command: "office-agent", timeout: null },
|
||||||
|
"thread1",
|
||||||
|
"Write a report",
|
||||||
|
spawnFn,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.outputDocx).toBe(outFile);
|
||||||
|
expect(result.sourceDocx).toBeNull();
|
||||||
|
expect(spawnFn.mock.calls[0][0]).toBe("office-agent");
|
||||||
|
expect(spawnFn.mock.calls[0][1]).toEqual(["create", "Write a report", "-o", "output.docx"]);
|
||||||
|
expect(spawnFn.mock.calls[0][2].cwd).toBe(join(base, "thread1"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses PATH office-agent when command is null", async () => {
|
||||||
|
const base = tempDir();
|
||||||
|
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
|
||||||
|
mkdirSync(join(base, "t2"), { recursive: true });
|
||||||
|
writeFileSync(join(base, "t2", "output.docx"), "");
|
||||||
|
|
||||||
|
await generateDocument(
|
||||||
|
{ outputDir: base, command: null, timeout: null },
|
||||||
|
"t2",
|
||||||
|
"Generate",
|
||||||
|
spawnFn,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(spawnFn.mock.calls[0][0]).toBe("office-agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws on non_zero_exit", async () => {
|
||||||
|
const base = tempDir();
|
||||||
|
const spawnFn = makeSpawn(
|
||||||
|
err({ kind: "non_zero_exit", exitCode: 1, stdout: "", stderr: "error" }) as MockSpawnResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
generateDocument({ outputDir: base, command: null, timeout: null }, "t3", "fail", spawnFn),
|
||||||
|
).rejects.toThrow("office-agent failed (exit 1)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws on timeout", async () => {
|
||||||
|
const base = tempDir();
|
||||||
|
const spawnFn = makeSpawn(err({ kind: "timeout" }) as MockSpawnResult);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
generateDocument({ outputDir: base, command: null, timeout: null }, "t4", "slow", spawnFn),
|
||||||
|
).rejects.toThrow("office-agent: timed out");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when output file not created", async () => {
|
||||||
|
const base = tempDir();
|
||||||
|
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
|
||||||
|
// Do NOT create output.docx
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
generateDocument({ outputDir: base, command: null, timeout: null }, "t5", "no file", spawnFn),
|
||||||
|
).rejects.toThrow("output file not found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editDocument", () => {
|
||||||
|
test("copies input to original.docx and modified.docx, calls edit, returns paths", async () => {
|
||||||
|
const base = tempDir();
|
||||||
|
// Create a fake inputDocx
|
||||||
|
const inputFile = join(base, "source.docx");
|
||||||
|
writeFileSync(inputFile, "original content");
|
||||||
|
|
||||||
|
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
|
||||||
|
// Simulate CLI overwriting modified.docx
|
||||||
|
const outDir = join(base, "te1");
|
||||||
|
mkdirSync(outDir, { recursive: true });
|
||||||
|
writeFileSync(join(outDir, "modified.docx"), "modified content");
|
||||||
|
|
||||||
|
const result = await editDocument(
|
||||||
|
{ outputDir: base, command: "office-agent", timeout: null },
|
||||||
|
"te1",
|
||||||
|
"Edit the doc",
|
||||||
|
inputFile,
|
||||||
|
spawnFn,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.outputDocx).toBe(join(outDir, "modified.docx"));
|
||||||
|
expect(result.sourceDocx).toBe(join(outDir, "original.docx"));
|
||||||
|
expect(spawnFn.mock.calls[0][1]).toEqual(["edit", "modified.docx", "Edit the doc"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws on spawn_failed", async () => {
|
||||||
|
const base = tempDir();
|
||||||
|
const inputFile = join(base, "src.docx");
|
||||||
|
writeFileSync(inputFile, "");
|
||||||
|
const spawnFn = makeSpawn(
|
||||||
|
err({ kind: "spawn_failed", message: "not found" }) as MockSpawnResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
editDocument({ outputDir: base, command: null, timeout: null }, "te2", "edit", inputFile, spawnFn),
|
||||||
|
).rejects.toThrow("spawn failed");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@uncaged/workflow-agent-office",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"files": ["src", "dist", "package.json"],
|
||||||
|
"type": "module",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"bun": "./src/index.ts",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/workflow-runtime": "workspace:^",
|
||||||
|
"@uncaged/workflow-util": "workspace:^",
|
||||||
|
"@uncaged/workflow-util-agent": "workspace:^"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import * as z from "zod/v4";
|
||||||
|
import type { AdapterFn, RoleResult, ThreadContext, WorkflowRuntime } from "@uncaged/workflow-runtime";
|
||||||
|
import { createLogger } from "@uncaged/workflow-util";
|
||||||
|
import { editDocument, generateDocument } from "./runner.js";
|
||||||
|
import type { OfficeAgentConfig } from "./types.js";
|
||||||
|
|
||||||
|
const log = createLogger({ sink: { kind: "stderr" } });
|
||||||
|
|
||||||
|
type ParsedInput = { prompt: string; inputDocx: string | null };
|
||||||
|
|
||||||
|
function parseStartInput(content: string): ParsedInput {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(content) as Record<string, unknown>;
|
||||||
|
if (typeof parsed.prompt === "string") {
|
||||||
|
return {
|
||||||
|
prompt: parsed.prompt,
|
||||||
|
inputDocx: typeof parsed.inputDocx === "string" ? parsed.inputDocx : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// not JSON — treat whole content as prompt, generate mode
|
||||||
|
}
|
||||||
|
return { prompt: content, inputDocx: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOfficeAgent(config: OfficeAgentConfig): AdapterFn {
|
||||||
|
return <T>(_systemPrompt: string, schema: z.ZodType<T>) =>
|
||||||
|
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
||||||
|
const { prompt, inputDocx } = parseStartInput(ctx.start.content);
|
||||||
|
log("8FQKP3NV", `office-agent: mode=${inputDocx === null ? "generate" : "edit"} thread=${ctx.threadId}`);
|
||||||
|
|
||||||
|
let raw: string;
|
||||||
|
if (inputDocx === null) {
|
||||||
|
const result = await generateDocument(config, ctx.threadId, prompt);
|
||||||
|
raw = JSON.stringify({ mode: "generate", outputDocx: result.outputDocx, sourceDocx: null });
|
||||||
|
} else {
|
||||||
|
const result = await editDocument(config, ctx.threadId, prompt, inputDocx);
|
||||||
|
raw = JSON.stringify({ mode: "edit", outputDocx: result.outputDocx, sourceDocx: result.sourceDocx });
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = schema.parse(JSON.parse(raw)) as T;
|
||||||
|
return { meta, childThread: null };
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { createOfficeAgent } from "./agent.js";
|
||||||
|
export { packageDescriptor } from "./package-descriptor.js";
|
||||||
|
export type { OfficeAgentConfig } from "./types.js";
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { PackageDescriptor } from "@uncaged/workflow-runtime";
|
||||||
|
|
||||||
|
export const packageDescriptor: PackageDescriptor = {
|
||||||
|
name: "@uncaged/workflow-agent-office",
|
||||||
|
version: "0.1.0",
|
||||||
|
capabilities: ["office-agent-cli", "docx-generate", "docx-edit"],
|
||||||
|
configSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["outputDir"],
|
||||||
|
properties: {
|
||||||
|
outputDir: {
|
||||||
|
type: "string",
|
||||||
|
description: "Root directory for workflow outputs; subdirs are created per threadId.",
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
anyOf: [{ type: "string" }, { type: "null" }],
|
||||||
|
description: "Path to office-agent CLI binary; null uses PATH.",
|
||||||
|
},
|
||||||
|
timeout: {
|
||||||
|
anyOf: [{ type: "number" }, { type: "null" }],
|
||||||
|
description: "Timeout in milliseconds; null means no limit.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { copyFile, mkdir, stat } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { spawnCli } from "@uncaged/workflow-util-agent";
|
||||||
|
import type { SpawnCliError } from "@uncaged/workflow-util-agent";
|
||||||
|
import type { OfficeAgentConfig } from "./types.js";
|
||||||
|
|
||||||
|
type SpawnCliFn = typeof spawnCli;
|
||||||
|
|
||||||
|
function throwSpawnError(e: SpawnCliError): never {
|
||||||
|
if (e.kind === "non_zero_exit")
|
||||||
|
throw new Error(`office-agent failed (exit ${e.exitCode}): ${e.stderr}`);
|
||||||
|
if (e.kind === "timeout")
|
||||||
|
throw new Error("office-agent: timed out");
|
||||||
|
throw new Error(`office-agent: spawn failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertFileExists(path: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await stat(path);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`office-agent: output file not found: ${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateDocument(
|
||||||
|
config: OfficeAgentConfig,
|
||||||
|
threadId: string,
|
||||||
|
prompt: string,
|
||||||
|
spawnCliFn: SpawnCliFn = spawnCli,
|
||||||
|
): Promise<{ outputDocx: string; sourceDocx: null }> {
|
||||||
|
const outputDir = join(config.outputDir, threadId);
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
const command = config.command ?? "office-agent";
|
||||||
|
const result = await spawnCliFn(command, ["create", prompt, "-o", "output.docx"], {
|
||||||
|
cwd: outputDir,
|
||||||
|
timeoutMs: config.timeout,
|
||||||
|
});
|
||||||
|
if (!result.ok) throwSpawnError(result.error);
|
||||||
|
const outputDocx = join(outputDir, "output.docx");
|
||||||
|
await assertFileExists(outputDocx);
|
||||||
|
return { outputDocx, sourceDocx: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editDocument(
|
||||||
|
config: OfficeAgentConfig,
|
||||||
|
threadId: string,
|
||||||
|
prompt: string,
|
||||||
|
inputDocx: string,
|
||||||
|
spawnCliFn: SpawnCliFn = spawnCli,
|
||||||
|
): Promise<{ outputDocx: string; sourceDocx: string }> {
|
||||||
|
const outputDir = join(config.outputDir, threadId);
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
const originalDocx = join(outputDir, "original.docx");
|
||||||
|
const modifiedDocx = join(outputDir, "modified.docx");
|
||||||
|
await copyFile(inputDocx, originalDocx);
|
||||||
|
await copyFile(inputDocx, modifiedDocx);
|
||||||
|
const command = config.command ?? "office-agent";
|
||||||
|
const result = await spawnCliFn(command, ["edit", "modified.docx", prompt], {
|
||||||
|
cwd: outputDir,
|
||||||
|
timeoutMs: config.timeout,
|
||||||
|
});
|
||||||
|
if (!result.ok) throwSpawnError(result.error);
|
||||||
|
await assertFileExists(modifiedDocx);
|
||||||
|
return { outputDocx: modifiedDocx, sourceDocx: originalDocx };
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type OfficeAgentConfig = {
|
||||||
|
outputDir: string;
|
||||||
|
command: string | null;
|
||||||
|
timeout: number | null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../workflow-protocol" },
|
||||||
|
{ "path": "../workflow-runtime" },
|
||||||
|
{ "path": "../workflow-util" },
|
||||||
|
{ "path": "../workflow-util-agent" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { tableToModerator } from "@uncaged/workflow-protocol/moderator-table.js";
|
||||||
|
import { validateWorkflowDescriptor } from "@uncaged/workflow-register";
|
||||||
|
import { END, type ModeratorContext, type RoleStep, START } from "@uncaged/workflow-runtime";
|
||||||
|
import { buildDocumentDescriptor } from "../src/descriptor.js";
|
||||||
|
import { documentTable } from "../src/moderator.js";
|
||||||
|
import type { DifferMeta, WriterMeta } from "../src/roles/index.js";
|
||||||
|
import type { DocumentMeta } from "../src/roles.js";
|
||||||
|
|
||||||
|
const documentModerator = tableToModerator(documentTable);
|
||||||
|
|
||||||
|
function makeCtx(
|
||||||
|
steps: ModeratorContext<DocumentMeta>["steps"],
|
||||||
|
): ModeratorContext<DocumentMeta> {
|
||||||
|
return {
|
||||||
|
threadId: "01TEST000000000000000000TR",
|
||||||
|
depth: 0,
|
||||||
|
bundleHash: "TESTHASH00001",
|
||||||
|
start: { role: START, content: "", meta: {}, timestamp: 0, parentState: null },
|
||||||
|
steps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writerGenerateStep(): RoleStep<DocumentMeta> {
|
||||||
|
return {
|
||||||
|
role: "writer",
|
||||||
|
contentHash: "STUBHASHWRITER001",
|
||||||
|
meta: { mode: "generate", outputDocx: "/out/output.docx", sourceDocx: null } satisfies WriterMeta,
|
||||||
|
refs: [],
|
||||||
|
timestamp: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writerEditStep(): RoleStep<DocumentMeta> {
|
||||||
|
return {
|
||||||
|
role: "writer",
|
||||||
|
contentHash: "STUBHASHWRITER002",
|
||||||
|
meta: { mode: "edit", outputDocx: "/out/modified.docx", sourceDocx: "/out/original.docx" } satisfies WriterMeta,
|
||||||
|
refs: [],
|
||||||
|
timestamp: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function differStep(): RoleStep<DocumentMeta> {
|
||||||
|
return {
|
||||||
|
role: "differ",
|
||||||
|
contentHash: "STUBHASHDIFF001",
|
||||||
|
meta: {
|
||||||
|
sourceDocx: "/out/original.docx",
|
||||||
|
modifiedDocx: "/out/modified.docx",
|
||||||
|
diffDocx: "/out/diff.docx",
|
||||||
|
} satisfies DifferMeta,
|
||||||
|
refs: [],
|
||||||
|
timestamp: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("documentTable", () => {
|
||||||
|
test("START → writer", () => {
|
||||||
|
expect(documentModerator(makeCtx([]))).toBe("writer");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("writer (generate) → END", () => {
|
||||||
|
expect(documentModerator(makeCtx([writerGenerateStep()]))).toBe(END);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("writer (edit) → differ", () => {
|
||||||
|
expect(documentModerator(makeCtx([writerEditStep()]))).toBe("differ");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("differ → END", () => {
|
||||||
|
expect(documentModerator(makeCtx([writerEditStep(), differStep()]))).toBe(END);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildDocumentDescriptor", () => {
|
||||||
|
test("descriptor passes validation", () => {
|
||||||
|
const descriptor = buildDocumentDescriptor();
|
||||||
|
expect(() => validateWorkflowDescriptor(descriptor)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("descriptor has writer and differ roles", () => {
|
||||||
|
const descriptor = buildDocumentDescriptor();
|
||||||
|
expect(Object.keys(descriptor.roles)).toContain("writer");
|
||||||
|
expect(Object.keys(descriptor.roles)).toContain("differ");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@uncaged/workflow-template-document",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"files": ["src", "dist", "package.json"],
|
||||||
|
"type": "module",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"bun": "./src/index.ts",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/workflow-register": "workspace:^",
|
||||||
|
"@uncaged/workflow-runtime": "workspace:^",
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@uncaged/workflow-protocol": "workspace:^"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { buildDescriptor } from "@uncaged/workflow-register";
|
||||||
|
import { documentTable } from "./moderator.js";
|
||||||
|
import { DOCUMENT_WORKFLOW_DESCRIPTION, documentRoles } from "./roles.js";
|
||||||
|
|
||||||
|
export function buildDocumentDescriptor() {
|
||||||
|
return buildDescriptor({
|
||||||
|
description: DOCUMENT_WORKFLOW_DESCRIPTION,
|
||||||
|
roles: documentRoles,
|
||||||
|
table: documentTable,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { WorkflowDefinition } from "@uncaged/workflow-runtime";
|
||||||
|
import { documentTable } from "./moderator.js";
|
||||||
|
import { DOCUMENT_WORKFLOW_DESCRIPTION, type DocumentMeta, documentRoles } from "./roles.js";
|
||||||
|
|
||||||
|
export { buildDocumentDescriptor } from "./descriptor.js";
|
||||||
|
export { documentTable } from "./moderator.js";
|
||||||
|
export {
|
||||||
|
type DifferMeta,
|
||||||
|
differMetaSchema,
|
||||||
|
differRole,
|
||||||
|
type WriterMeta,
|
||||||
|
writerMetaSchema,
|
||||||
|
writerRole,
|
||||||
|
} from "./roles/index.js";
|
||||||
|
export {
|
||||||
|
DOCUMENT_WORKFLOW_DESCRIPTION,
|
||||||
|
type DocumentMeta,
|
||||||
|
type DocumentRoles,
|
||||||
|
documentRoles,
|
||||||
|
} from "./roles.js";
|
||||||
|
export type { DocumentStartInput } from "./types.js";
|
||||||
|
|
||||||
|
export const documentWorkflowDefinition: WorkflowDefinition<DocumentMeta> = {
|
||||||
|
description: DOCUMENT_WORKFLOW_DESCRIPTION,
|
||||||
|
roles: documentRoles,
|
||||||
|
table: documentTable,
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
END,
|
||||||
|
type ModeratorCondition,
|
||||||
|
type ModeratorTable,
|
||||||
|
START,
|
||||||
|
} from "@uncaged/workflow-runtime";
|
||||||
|
import type { WriterMeta } from "./roles/writer.js";
|
||||||
|
import type { DocumentMeta } from "./roles.js";
|
||||||
|
|
||||||
|
const writerIsEditMode: ModeratorCondition<DocumentMeta> = {
|
||||||
|
name: "writerIsEditMode",
|
||||||
|
description: "Writer ran in edit mode and produced a modified document",
|
||||||
|
check: (ctx) => {
|
||||||
|
const writerStep = ctx.steps.find((s) => s.role === "writer");
|
||||||
|
if (writerStep === undefined) return false;
|
||||||
|
return (writerStep.meta as WriterMeta).mode === "edit";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const documentTable: ModeratorTable<DocumentMeta> = {
|
||||||
|
[START]: [{ condition: "FALLBACK", role: "writer" }],
|
||||||
|
writer: [
|
||||||
|
{ condition: writerIsEditMode, role: "differ" },
|
||||||
|
{ condition: "FALLBACK", role: END },
|
||||||
|
],
|
||||||
|
differ: [{ condition: "FALLBACK", role: END }],
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { RoleDefinition } from "@uncaged/workflow-runtime";
|
||||||
|
import { type DifferMeta, differRole } from "./roles/differ.js";
|
||||||
|
import { type WriterMeta, writerRole } from "./roles/writer.js";
|
||||||
|
|
||||||
|
export const DOCUMENT_WORKFLOW_DESCRIPTION =
|
||||||
|
"Generates a new Word document from a prompt, or edits an existing one and produces a diff report.";
|
||||||
|
|
||||||
|
export type DocumentMeta = {
|
||||||
|
writer: WriterMeta;
|
||||||
|
differ: DifferMeta;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocumentRoles = {
|
||||||
|
[K in keyof DocumentMeta]: RoleDefinition<DocumentMeta[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const documentRoles: DocumentRoles = {
|
||||||
|
writer: writerRole,
|
||||||
|
differ: differRole,
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { RoleDefinition } from "@uncaged/workflow-runtime";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
export const differMetaSchema = z.object({
|
||||||
|
sourceDocx: z.string(),
|
||||||
|
modifiedDocx: z.string(),
|
||||||
|
diffDocx: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DifferMeta = z.infer<typeof differMetaSchema>;
|
||||||
|
|
||||||
|
export const differRole: RoleDefinition<DifferMeta> = {
|
||||||
|
description: "Produces a Word-format diff report of the writer's changes (edit mode only).",
|
||||||
|
systemPrompt: "",
|
||||||
|
schema: differMetaSchema,
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type { DifferMeta } from "./differ.js";
|
||||||
|
export { differMetaSchema, differRole } from "./differ.js";
|
||||||
|
export type { WriterMeta } from "./writer.js";
|
||||||
|
export { writerMetaSchema, writerRole } from "./writer.js";
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { RoleDefinition } from "@uncaged/workflow-runtime";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
export const writerMetaSchema = z.discriminatedUnion("mode", [
|
||||||
|
z.object({
|
||||||
|
mode: z.literal("generate"),
|
||||||
|
outputDocx: z.string(),
|
||||||
|
sourceDocx: z.null(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
mode: z.literal("edit"),
|
||||||
|
outputDocx: z.string(),
|
||||||
|
sourceDocx: z.string(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type WriterMeta = z.infer<typeof writerMetaSchema>;
|
||||||
|
|
||||||
|
export const writerRole: RoleDefinition<WriterMeta> = {
|
||||||
|
description: "Generates or modifies a Word document via an external agent.",
|
||||||
|
systemPrompt: "",
|
||||||
|
schema: writerMetaSchema,
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type DocumentStartInput = {
|
||||||
|
prompt: string;
|
||||||
|
inputDocx: string | null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../workflow-protocol" },
|
||||||
|
{ "path": "../workflow-runtime" },
|
||||||
|
{ "path": "../workflow-register" }
|
||||||
|
]
|
||||||
|
}
|
||||||
+17
-1
@@ -1,11 +1,27 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
// Mock agent for smoke testing
|
// Mock agent for smoke testing
|
||||||
|
import { bootstrap, type JSONSchema, putSchema } from "@uncaged/json-cas";
|
||||||
import { createAgent } from "../packages/uwf-agent-kit/src/index.js";
|
import { createAgent } from "../packages/uwf-agent-kit/src/index.js";
|
||||||
|
|
||||||
|
const MOCK_RAW_OUTPUT_SCHEMA: JSONSchema = {
|
||||||
|
title: "mock-raw-output",
|
||||||
|
type: "object",
|
||||||
|
required: ["text"],
|
||||||
|
properties: {
|
||||||
|
text: { type: "string" },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
const agent = createAgent({
|
const agent = createAgent({
|
||||||
name: "mock",
|
name: "mock",
|
||||||
run: async (ctx) => {
|
run: async (ctx) => {
|
||||||
return `Mock output for role ${ctx.role}: task was "${ctx.prompt}"`;
|
const output = `Mock output for role ${ctx.role}: task was "${ctx.start.prompt}"`;
|
||||||
|
const { store } = ctx;
|
||||||
|
await bootstrap(store);
|
||||||
|
const schemaHash = await putSchema(store, MOCK_RAW_OUTPUT_SCHEMA);
|
||||||
|
const detailHash = await store.put(schemaHash, { text: output });
|
||||||
|
return { output, detailHash };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -37,6 +37,9 @@
|
|||||||
{ "path": "packages/uwf-moderator" },
|
{ "path": "packages/uwf-moderator" },
|
||||||
{ "path": "packages/cli-uwf" },
|
{ "path": "packages/cli-uwf" },
|
||||||
{ "path": "packages/uwf-agent-kit" },
|
{ "path": "packages/uwf-agent-kit" },
|
||||||
{ "path": "packages/uwf-agent-hermes" }
|
{ "path": "packages/uwf-agent-hermes" },
|
||||||
|
{ "path": "packages/workflow-template-document" },
|
||||||
|
{ "path": "packages/workflow-agent-office" },
|
||||||
|
{ "path": "packages/workflow-agent-docx-diff" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user