refactor(workflow): decouple agent context from CAS and fix monorepo checks
Move CAS access into extract dependencies so AgentContext stays state-only, and clean up type/lint/check regressions across CLI/dashboard to keep full check green. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||||
"files": {
|
"files": {
|
||||||
"includes": ["**", "!**/dist", "!**/node_modules"]
|
"includes": ["**", "!**/dist", "!**/node_modules", "!packages/workflow/workflow"]
|
||||||
},
|
},
|
||||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ export function createLiveRoutes(storageRoot: string): Hono {
|
|||||||
if (dataPath === null) {
|
if (dataPath === null) {
|
||||||
return c.json({ error: `thread not found: ${threadId}` }, 404);
|
return c.json({ error: `thread not found: ${threadId}` }, 404);
|
||||||
}
|
}
|
||||||
|
const resolvedDataPath = dataPath;
|
||||||
|
|
||||||
const infoPath = join(dirname(dataPath), `${threadId}.info.jsonl`);
|
const infoPath = join(dirname(resolvedDataPath), `${threadId}.info.jsonl`);
|
||||||
|
|
||||||
return streamSSE(c, async (stream) => {
|
return streamSSE(c, async (stream) => {
|
||||||
const dataState: PumpState = { contentOffset: 0, carry: "" };
|
const dataState: PumpState = { contentOffset: 0, carry: "" };
|
||||||
@@ -71,7 +72,7 @@ export function createLiveRoutes(storageRoot: string): Hono {
|
|||||||
async function pumpData(): Promise<boolean> {
|
async function pumpData(): Promise<boolean> {
|
||||||
let text: string;
|
let text: string;
|
||||||
try {
|
try {
|
||||||
text = await readFile(dataPath, "utf8");
|
text = await readFile(resolvedDataPath, "utf8");
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,7 @@ export function createLiveRoutes(storageRoot: string): Hono {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
let completed = false;
|
let completed = false;
|
||||||
|
|
||||||
const dataWatcher = watch(dataPath, async () => {
|
const dataWatcher = watch(resolvedDataPath, async () => {
|
||||||
if (completed) return;
|
if (completed) return;
|
||||||
const finished = await pumpData();
|
const finished = await pumpData();
|
||||||
if (finished) {
|
if (finished) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ async function postJson<T>(path: string, body: unknown): Promise<T> {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({ error: res.statusText })) as { error: string };
|
const err = (await res.json().catch(() => ({ error: res.statusText }))) as { error: string };
|
||||||
throw new Error(err.error || `API ${res.status}`);
|
throw new Error(err.error || `API ${res.status}`);
|
||||||
}
|
}
|
||||||
return res.json() as Promise<T>;
|
return res.json() as Promise<T>;
|
||||||
@@ -59,7 +59,11 @@ export function getThread(id: string): Promise<{ records: ThreadRecord[] }> {
|
|||||||
return fetchJson(`/threads/${id}`);
|
return fetchJson(`/threads/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runThread(workflow: string, prompt: string, maxRounds: number = 10): Promise<{ threadId: string }> {
|
export function runThread(
|
||||||
|
workflow: string,
|
||||||
|
prompt: string,
|
||||||
|
maxRounds: number = 10,
|
||||||
|
): Promise<{ threadId: string }> {
|
||||||
return postJson("/threads", { workflow, prompt, maxRounds });
|
return postJson("/threads", { workflow, prompt, maxRounds });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Sidebar } from "./components/sidebar.tsx";
|
|
||||||
import { ThreadList } from "./components/thread-list.tsx";
|
|
||||||
import { ThreadDetail } from "./components/thread-detail.tsx";
|
|
||||||
import { WorkflowList } from "./components/workflow-list.tsx";
|
|
||||||
import { StatusBar } from "./components/status-bar.tsx";
|
|
||||||
import { RunDialog } from "./components/run-dialog.tsx";
|
import { RunDialog } from "./components/run-dialog.tsx";
|
||||||
|
import { Sidebar } from "./components/sidebar.tsx";
|
||||||
|
import { StatusBar } from "./components/status-bar.tsx";
|
||||||
|
import { ThreadDetail } from "./components/thread-detail.tsx";
|
||||||
|
import { ThreadList } from "./components/thread-list.tsx";
|
||||||
|
import { WorkflowList } from "./components/workflow-list.tsx";
|
||||||
|
|
||||||
type View = "threads" | "workflows";
|
type View = "threads" | "workflows";
|
||||||
|
|
||||||
@@ -19,9 +19,7 @@ export function App() {
|
|||||||
<main className="flex-1 overflow-hidden flex flex-col">
|
<main className="flex-1 overflow-hidden flex flex-col">
|
||||||
<StatusBar onRun={() => setShowRun(true)} />
|
<StatusBar onRun={() => setShowRun(true)} />
|
||||||
<div className="flex-1 overflow-auto p-6">
|
<div className="flex-1 overflow-auto p-6">
|
||||||
{view === "threads" && !selectedThread && (
|
{view === "threads" && !selectedThread && <ThreadList onSelect={setSelectedThread} />}
|
||||||
<ThreadList onSelect={setSelectedThread} />
|
|
||||||
)}
|
|
||||||
{view === "threads" && selectedThread && (
|
{view === "threads" && selectedThread && (
|
||||||
<ThreadDetail threadId={selectedThread} onBack={() => setSelectedThread(null)} />
|
<ThreadDetail threadId={selectedThread} onBack={() => setSelectedThread(null)} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,10 +41,15 @@ export function RunDialog({ onClose, onCreated }: Props) {
|
|||||||
<h3 className="text-lg font-semibold mb-4">Run Thread</h3>
|
<h3 className="text-lg font-semibold mb-4">Run Thread</h3>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm block mb-1" style={{ color: "var(--color-text-muted)" }}>
|
<label
|
||||||
|
htmlFor="run-workflow"
|
||||||
|
className="text-sm block mb-1"
|
||||||
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
|
>
|
||||||
Workflow
|
Workflow
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
id="run-workflow"
|
||||||
value={workflow}
|
value={workflow}
|
||||||
onChange={(e) => setWorkflow(e.target.value)}
|
onChange={(e) => setWorkflow(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded border text-sm"
|
className="w-full px-3 py-2 rounded border text-sm"
|
||||||
@@ -64,10 +69,15 @@ export function RunDialog({ onClose, onCreated }: Props) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm block mb-1" style={{ color: "var(--color-text-muted)" }}>
|
<label
|
||||||
|
htmlFor="run-prompt"
|
||||||
|
className="text-sm block mb-1"
|
||||||
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
|
>
|
||||||
Prompt
|
Prompt
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
id="run-prompt"
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
rows={4}
|
rows={4}
|
||||||
@@ -81,10 +91,15 @@ export function RunDialog({ onClose, onCreated }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm block mb-1" style={{ color: "var(--color-text-muted)" }}>
|
<label
|
||||||
|
htmlFor="run-max-rounds"
|
||||||
|
className="text-sm block mb-1"
|
||||||
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
|
>
|
||||||
Max Rounds
|
Max Rounds
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="run-max-rounds"
|
||||||
type="number"
|
type="number"
|
||||||
value={maxRounds}
|
value={maxRounds}
|
||||||
onChange={(e) => setMaxRounds(Number(e.target.value))}
|
onChange={(e) => setMaxRounds(Number(e.target.value))}
|
||||||
@@ -98,7 +113,11 @@ export function RunDialog({ onClose, onCreated }: Props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-sm" style={{ color: "var(--color-error)" }}>{error}</p>}
|
{error && (
|
||||||
|
<p className="text-sm" style={{ color: "var(--color-error)" }}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="flex gap-2 justify-end">
|
<div className="flex gap-2 justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -10,16 +10,22 @@ export function Sidebar({ view, onViewChange }: Props) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-56 border-r flex flex-col" style={{ borderColor: "var(--color-border)", background: "var(--color-surface)" }}>
|
<aside
|
||||||
|
className="w-56 border-r flex flex-col"
|
||||||
|
style={{ borderColor: "var(--color-border)", background: "var(--color-surface)" }}
|
||||||
|
>
|
||||||
<div className="p-4 border-b" style={{ borderColor: "var(--color-border)" }}>
|
<div className="p-4 border-b" style={{ borderColor: "var(--color-border)" }}>
|
||||||
<h1 className="text-lg font-semibold" style={{ color: "var(--color-accent)" }}>
|
<h1 className="text-lg font-semibold" style={{ color: "var(--color-accent)" }}>
|
||||||
⚙ Workflow
|
⚙ Workflow
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs mt-1" style={{ color: "var(--color-text-muted)" }}>Dashboard</p>
|
<p className="text-xs mt-1" style={{ color: "var(--color-text-muted)" }}>
|
||||||
|
Dashboard
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex-1 p-2 space-y-1">
|
<nav className="flex-1 p-2 space-y-1">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={item.key}
|
key={item.key}
|
||||||
onClick={() => onViewChange(item.key)}
|
onClick={() => onViewChange(item.key)}
|
||||||
className="w-full text-left px-3 py-2 rounded text-sm transition-colors"
|
className="w-full text-left px-3 py-2 rounded text-sm transition-colors"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function StatusBar({ onRun }: Props) {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span style={{ color: "var(--color-text-muted)" }}>Local API: 127.0.0.1:7860</span>
|
<span style={{ color: "var(--color-text-muted)" }}>Local API: 127.0.0.1:7860</span>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onRun}
|
onClick={onRun}
|
||||||
className="px-3 py-1 rounded text-xs font-medium"
|
className="px-3 py-1 rounded text-xs font-medium"
|
||||||
style={{ background: "var(--color-accent)", color: "#fff" }}
|
style={{ background: "var(--color-accent)", color: "#fff" }}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function ThreadDetail({ threadId, onBack }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="text-sm hover:underline"
|
className="text-sm hover:underline"
|
||||||
style={{ color: "var(--color-accent)" }}
|
style={{ color: "var(--color-accent)" }}
|
||||||
@@ -34,6 +35,7 @@ export function ThreadDetail({ threadId, onBack }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handleAction("pause")}
|
onClick={() => handleAction("pause")}
|
||||||
className="px-3 py-1 text-xs rounded border"
|
className="px-3 py-1 text-xs rounded border"
|
||||||
style={{ borderColor: "var(--color-warning)", color: "var(--color-warning)" }}
|
style={{ borderColor: "var(--color-warning)", color: "var(--color-warning)" }}
|
||||||
@@ -41,6 +43,7 @@ export function ThreadDetail({ threadId, onBack }: Props) {
|
|||||||
⏸ Pause
|
⏸ Pause
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handleAction("resume")}
|
onClick={() => handleAction("resume")}
|
||||||
className="px-3 py-1 text-xs rounded border"
|
className="px-3 py-1 text-xs rounded border"
|
||||||
style={{ borderColor: "var(--color-success)", color: "var(--color-success)" }}
|
style={{ borderColor: "var(--color-success)", color: "var(--color-success)" }}
|
||||||
@@ -48,6 +51,7 @@ export function ThreadDetail({ threadId, onBack }: Props) {
|
|||||||
▶ Resume
|
▶ Resume
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handleAction("kill")}
|
onClick={() => handleAction("kill")}
|
||||||
className="px-3 py-1 text-xs rounded border"
|
className="px-3 py-1 text-xs rounded border"
|
||||||
style={{ borderColor: "var(--color-error)", color: "var(--color-error)" }}
|
style={{ borderColor: "var(--color-error)", color: "var(--color-error)" }}
|
||||||
@@ -68,9 +72,9 @@ export function ThreadDetail({ threadId, onBack }: Props) {
|
|||||||
{status === "error" && <p style={{ color: "var(--color-error)" }}>Error: {error}</p>}
|
{status === "error" && <p style={{ color: "var(--color-error)" }}>Error: {error}</p>}
|
||||||
{status === "ok" && (
|
{status === "ok" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{data.records.map((r, i) => (
|
{data.records.map((r) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={`${r.type}:${r.role ?? ""}:${r.timestamp ?? 0}:${String(r.content ?? "")}`}
|
||||||
className="p-3 rounded border text-sm"
|
className="p-3 rounded border text-sm"
|
||||||
style={{ background: "var(--color-surface)", borderColor: "var(--color-border)" }}
|
style={{ background: "var(--color-surface)", borderColor: "var(--color-border)" }}
|
||||||
>
|
>
|
||||||
@@ -93,7 +97,10 @@ export function ThreadDetail({ threadId, onBack }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{r.content && (
|
{r.content && (
|
||||||
<pre className="whitespace-pre-wrap text-xs mt-1" style={{ color: "var(--color-text)" }}>
|
<pre
|
||||||
|
className="whitespace-pre-wrap text-xs mt-1"
|
||||||
|
style={{ color: "var(--color-text)" }}
|
||||||
|
>
|
||||||
{typeof r.content === "string" ? r.content : JSON.stringify(r.content, null, 2)}
|
{typeof r.content === "string" ? r.content : JSON.stringify(r.content, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ type Props = {
|
|||||||
export function ThreadList({ onSelect }: Props) {
|
export function ThreadList({ onSelect }: Props) {
|
||||||
const { status, data, error } = useFetch(() => listThreads(), []);
|
const { status, data, error } = useFetch(() => listThreads(), []);
|
||||||
|
|
||||||
if (status === "loading") return <p style={{ color: "var(--color-text-muted)" }}>Loading threads...</p>;
|
if (status === "loading")
|
||||||
|
return <p style={{ color: "var(--color-text-muted)" }}>Loading threads...</p>;
|
||||||
if (status === "error") return <p style={{ color: "var(--color-error)" }}>Error: {error}</p>;
|
if (status === "error") return <p style={{ color: "var(--color-error)" }}>Error: {error}</p>;
|
||||||
|
|
||||||
const threads = data.threads;
|
const threads = data.threads;
|
||||||
@@ -22,6 +23,7 @@ export function ThreadList({ onSelect }: Props) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{threads.map((t) => (
|
{threads.map((t) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={t.threadId}
|
key={t.threadId}
|
||||||
onClick={() => onSelect(t.threadId)}
|
onClick={() => onSelect(t.threadId)}
|
||||||
className="w-full text-left p-4 rounded-lg border transition-colors hover:border-[var(--color-accent-dim)]"
|
className="w-full text-left p-4 rounded-lg border transition-colors hover:border-[var(--color-accent-dim)]"
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { useFetch } from "../hooks.ts";
|
|||||||
export function WorkflowList() {
|
export function WorkflowList() {
|
||||||
const { status, data, error } = useFetch(() => listWorkflows(), []);
|
const { status, data, error } = useFetch(() => listWorkflows(), []);
|
||||||
|
|
||||||
if (status === "loading") return <p style={{ color: "var(--color-text-muted)" }}>Loading workflows...</p>;
|
if (status === "loading")
|
||||||
|
return <p style={{ color: "var(--color-text-muted)" }}>Loading workflows...</p>;
|
||||||
if (status === "error") return <p style={{ color: "var(--color-error)" }}>Error: {error}</p>;
|
if (status === "error") return <p style={{ color: "var(--color-error)" }}>Error: {error}</p>;
|
||||||
|
|
||||||
const workflows = data.workflows;
|
const workflows = data.workflows;
|
||||||
@@ -28,7 +29,10 @@ export function WorkflowList() {
|
|||||||
{w.versions} version{w.versions !== 1 ? "s" : ""}
|
{w.versions} version{w.versions !== 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<code className="text-xs mt-1 block font-mono" style={{ color: "var(--color-accent)" }}>
|
<code
|
||||||
|
className="text-xs mt-1 block font-mono"
|
||||||
|
style={{ color: "var(--color-accent)" }}
|
||||||
|
>
|
||||||
{w.currentHash}
|
{w.currentHash}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function useFetch<T>(fetcher: () => Promise<T>, deps: unknown[] = []): Fe
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: this helper intentionally accepts caller-provided dependency arrays
|
||||||
}, deps);
|
}, deps);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// biome-ignore lint/style/noDefaultExport: Vite loads config from default export.
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { mkdtempSync } from "node:fs";
|
import { type AgentContext, START } from "@uncaged/workflow-runtime";
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { createCasStore } from "@uncaged/workflow";
|
|
||||||
import { START, type AgentContext } from "@uncaged/workflow-runtime";
|
|
||||||
|
|
||||||
import { createLlmAdapter } from "../src/create-llm-adapter.js";
|
import { createLlmAdapter } from "../src/create-llm-adapter.js";
|
||||||
|
|
||||||
const casDir = mkdtempSync(join(tmpdir(), "wf-llm-adapter-cas-"));
|
|
||||||
const testCas = createCasStore(casDir);
|
|
||||||
|
|
||||||
function makeCtx(userContent: string): AgentContext {
|
function makeCtx(userContent: string): AgentContext {
|
||||||
return {
|
return {
|
||||||
start: {
|
start: {
|
||||||
@@ -22,7 +15,6 @@ function makeCtx(userContent: string): AgentContext {
|
|||||||
steps: [],
|
steps: [],
|
||||||
threadId: "01TEST000000000000000000TR",
|
threadId: "01TEST000000000000000000TR",
|
||||||
currentRole: { name: "planner", systemPrompt: "system instructions" },
|
currentRole: { name: "planner", systemPrompt: "system instructions" },
|
||||||
cas: testCas,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ async function advanceOneRound<M extends RoleMeta>(
|
|||||||
const agentCtx: AgentContext<M> = {
|
const agentCtx: AgentContext<M> = {
|
||||||
...modCtx,
|
...modCtx,
|
||||||
currentRole: { name: next, systemPrompt: roleDef.systemPrompt },
|
currentRole: { name: next, systemPrompt: roleDef.systemPrompt },
|
||||||
cas: runtime.cas,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const agent = agentForRole(binding, next);
|
const agent = agentForRole(binding, next);
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export type {
|
|||||||
WorkflowCompletion,
|
WorkflowCompletion,
|
||||||
WorkflowDefinition,
|
WorkflowDefinition,
|
||||||
WorkflowFn,
|
WorkflowFn,
|
||||||
WorkflowRuntime,
|
|
||||||
WorkflowResult,
|
WorkflowResult,
|
||||||
|
WorkflowRuntime,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
export { END, START } from "./types.js";
|
export { END, START } from "./types.js";
|
||||||
export type { Result } from "./util/index.js";
|
export type { Result } from "./util/index.js";
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ export type AgentContext<M extends RoleMeta = RoleMeta> = ModeratorContext<M> &
|
|||||||
name: string;
|
name: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
};
|
};
|
||||||
cas: CasStore;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Phase 3: Extractor runs — has agent output; the extraction instruction is a separate argument to the extract function. */
|
/** Phase 3: Extractor runs — has agent output; the extraction instruction is a separate argument to the extract function. */
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ function buildToolCallResponse(args: Record<string, unknown>): Response {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function installMockToolCallCompletions(sequence: ReadonlyArray<Record<string, unknown>>): () => void {
|
function installMockToolCallCompletions(
|
||||||
|
sequence: ReadonlyArray<Record<string, unknown>>,
|
||||||
|
): () => void {
|
||||||
const origFetch = globalThis.fetch;
|
const origFetch = globalThis.fetch;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const mockFetch = async (
|
const mockFetch = async (
|
||||||
@@ -160,11 +162,16 @@ function submitterStep(meta: SubmitterMeta): RoleStep<SolveIssueMeta> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const stubExtract = createExtract({
|
function createStubExtract(casDir: string) {
|
||||||
baseUrl: "http://127.0.0.1:9",
|
return createExtract(
|
||||||
apiKey: "",
|
{
|
||||||
model: "test",
|
baseUrl: "http://127.0.0.1:9",
|
||||||
});
|
apiKey: "",
|
||||||
|
model: "test",
|
||||||
|
},
|
||||||
|
{ cas: createCasStore(casDir) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function makeThread(prompt: string) {
|
function makeThread(prompt: string) {
|
||||||
return {
|
return {
|
||||||
@@ -260,13 +267,10 @@ describe("solveIssueWorkflowDefinition + createWorkflow", () => {
|
|||||||
agent: async () => "",
|
agent: async () => "",
|
||||||
overrides: { developer: async () => "stub-root-hash" },
|
overrides: { developer: async () => "stub-root-hash" },
|
||||||
});
|
});
|
||||||
const gen = run(
|
const gen = run(makeThread("task"), {
|
||||||
makeThread("task"),
|
cas,
|
||||||
{
|
extract: createStubExtract(casDir),
|
||||||
cas,
|
});
|
||||||
extract: stubExtract,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const first = await gen.next();
|
const first = await gen.next();
|
||||||
expect(first.done).toBe(false);
|
expect(first.done).toBe(false);
|
||||||
if (first.done) {
|
if (first.done) {
|
||||||
@@ -297,13 +301,10 @@ describe("solveIssueWorkflowDefinition + createWorkflow", () => {
|
|||||||
agent: async () => "",
|
agent: async () => "",
|
||||||
overrides: { developer: async () => "stub-root-hash" },
|
overrides: { developer: async () => "stub-root-hash" },
|
||||||
});
|
});
|
||||||
const gen = run(
|
const gen = run(makeThread("task"), {
|
||||||
makeThread("task"),
|
cas,
|
||||||
{
|
extract: createStubExtract(casDir),
|
||||||
cas,
|
});
|
||||||
extract: stubExtract,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const first = await gen.next();
|
const first = await gen.next();
|
||||||
expect(first.done).toBe(false);
|
expect(first.done).toBe(false);
|
||||||
if (first.done) {
|
if (first.done) {
|
||||||
@@ -356,13 +357,10 @@ describe("solveIssueWorkflowDefinition + createWorkflow", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const gen = run(
|
const gen = run(makeThread("task"), {
|
||||||
makeThread("task"),
|
cas,
|
||||||
{
|
extract: createStubExtract(casDir),
|
||||||
cas,
|
});
|
||||||
extract: stubExtract,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await gen.next();
|
await gen.next();
|
||||||
expect(calls).toEqual(["preparer"]);
|
expect(calls).toEqual(["preparer"]);
|
||||||
|
|
||||||
@@ -374,7 +372,6 @@ describe("solveIssueWorkflowDefinition + createWorkflow", () => {
|
|||||||
await gen.next();
|
await gen.next();
|
||||||
expect(calls).toEqual(["submitter"]);
|
expect(calls).toEqual(["submitter"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildSolveIssueDescriptor", () => {
|
describe("buildSolveIssueDescriptor", () => {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { mkdtemp, rm } from "node:fs/promises";
|
import { type AgentContext, START } from "@uncaged/workflow-runtime";
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { createCasStore, putContentMerkleNode } from "@uncaged/workflow";
|
|
||||||
import { START, type AgentContext } from "@uncaged/workflow-runtime";
|
|
||||||
|
|
||||||
import { buildAgentPrompt } from "../src/index.js";
|
import { buildAgentPrompt } from "../src/index.js";
|
||||||
|
|
||||||
@@ -17,25 +13,13 @@ function startTask(content: string): AgentContext["start"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("buildAgentPrompt", () => {
|
describe("buildAgentPrompt", () => {
|
||||||
let casRoot: string;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
casRoot = await mkdtemp(join(tmpdir(), "wf-build-prompt-cas-"));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await rm(casRoot, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("includes system prompt and full task; omits tools when there are no steps", async () => {
|
test("includes system prompt and full task; omits tools when there are no steps", async () => {
|
||||||
const cas = createCasStore(casRoot);
|
|
||||||
const ctx: AgentContext = {
|
const ctx: AgentContext = {
|
||||||
start: startTask("fix the bug"),
|
start: startTask("fix the bug"),
|
||||||
depth: 0,
|
depth: 0,
|
||||||
steps: [],
|
steps: [],
|
||||||
threadId: "01TEST000000000000000000TR",
|
threadId: "01TEST000000000000000000TR",
|
||||||
currentRole: { name: START, systemPrompt: "You are an agent." },
|
currentRole: { name: START, systemPrompt: "You are an agent." },
|
||||||
cas,
|
|
||||||
};
|
};
|
||||||
const text = await buildAgentPrompt(ctx);
|
const text = await buildAgentPrompt(ctx);
|
||||||
expect(text).toContain("You are an agent.");
|
expect(text).toContain("You are an agent.");
|
||||||
@@ -44,15 +28,13 @@ describe("buildAgentPrompt", () => {
|
|||||||
expect(text).not.toContain("## Tools");
|
expect(text).not.toContain("## Tools");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("single step shows full content and meta, and includes tools", async () => {
|
test("single step shows hash and meta, and includes tools", async () => {
|
||||||
const cas = createCasStore(casRoot);
|
const onlyHash = "01HASHSINGLESTEP0000000001";
|
||||||
const onlyHash = await putContentMerkleNode(cas, "only step full body");
|
|
||||||
const ctx: AgentContext = {
|
const ctx: AgentContext = {
|
||||||
start: startTask("user task"),
|
start: startTask("user task"),
|
||||||
depth: 0,
|
depth: 0,
|
||||||
threadId: "01TEST000000000000000000TR",
|
threadId: "01TEST000000000000000000TR",
|
||||||
currentRole: { name: "coder", systemPrompt: "Be helpful." },
|
currentRole: { name: "coder", systemPrompt: "Be helpful." },
|
||||||
cas,
|
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
role: "coder",
|
role: "coder",
|
||||||
@@ -67,22 +49,20 @@ describe("buildAgentPrompt", () => {
|
|||||||
expect(text).toContain("## Task");
|
expect(text).toContain("## Task");
|
||||||
expect(text).toContain("user task");
|
expect(text).toContain("user task");
|
||||||
expect(text).toContain("## Step: coder");
|
expect(text).toContain("## Step: coder");
|
||||||
expect(text).toContain("only step full body");
|
expect(text).toContain(`ContentHash: ${onlyHash}`);
|
||||||
expect(text).toContain('Meta: {"files":["a.ts"]}');
|
expect(text).toContain('Meta: {"files":["a.ts"]}');
|
||||||
expect(text).toContain("## Tools");
|
expect(text).toContain("## Tools");
|
||||||
expect(text).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
expect(text).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("two or more steps: previous steps are meta-only; latest step is full", async () => {
|
test("two or more steps: previous steps are meta-only; latest step includes hash", async () => {
|
||||||
const cas = createCasStore(casRoot);
|
const plannerHash = "01HASHPLANNER0000000000001";
|
||||||
const plannerHash = await putContentMerkleNode(cas, "PLANNER_SECRET_FULL_TEXT");
|
const coderHash = "01HASHCODER0000000000000001";
|
||||||
const coderHash = await putContentMerkleNode(cas, "last step full content");
|
|
||||||
const ctx: AgentContext = {
|
const ctx: AgentContext = {
|
||||||
start: startTask("first message full: task content here"),
|
start: startTask("first message full: task content here"),
|
||||||
depth: 0,
|
depth: 0,
|
||||||
threadId: "01TEST000000000000000000TR",
|
threadId: "01TEST000000000000000000TR",
|
||||||
currentRole: { name: "coder", systemPrompt: "System." },
|
currentRole: { name: "coder", systemPrompt: "System." },
|
||||||
cas,
|
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
role: "planner",
|
role: "planner",
|
||||||
@@ -105,25 +85,22 @@ describe("buildAgentPrompt", () => {
|
|||||||
expect(text).toContain("## Previous Steps");
|
expect(text).toContain("## Previous Steps");
|
||||||
expect(text).toContain("### Step 1: planner");
|
expect(text).toContain("### Step 1: planner");
|
||||||
expect(text).toContain('Summary: {"plan":"short"}');
|
expect(text).toContain('Summary: {"plan":"short"}');
|
||||||
expect(text).not.toContain("PLANNER_SECRET_FULL_TEXT");
|
|
||||||
expect(text).toContain("## Latest Step: coder");
|
expect(text).toContain("## Latest Step: coder");
|
||||||
expect(text).toContain("last step full content");
|
expect(text).toContain(`ContentHash: ${coderHash}`);
|
||||||
expect(text).toContain('Meta: {"done":true}');
|
expect(text).toContain('Meta: {"done":true}');
|
||||||
expect(text).toContain("## Tools");
|
expect(text).toContain("## Tools");
|
||||||
expect(text).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
expect(text).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("middle steps show meta summary only, not full content", async () => {
|
test("middle steps show meta summary only and latest shows hash", async () => {
|
||||||
const cas = createCasStore(casRoot);
|
const ha = "01HASHA00000000000000000001";
|
||||||
const ha = await putContentMerkleNode(cas, "HIDDEN_A");
|
const hb = "01HASHB00000000000000000001";
|
||||||
const hb = await putContentMerkleNode(cas, "HIDDEN_B_MIDDLE");
|
const hc = "01HASHC00000000000000000001";
|
||||||
const hc = await putContentMerkleNode(cas, "VISIBLE_LAST");
|
|
||||||
const ctx: AgentContext = {
|
const ctx: AgentContext = {
|
||||||
start: startTask("start"),
|
start: startTask("start"),
|
||||||
depth: 0,
|
depth: 0,
|
||||||
threadId: "01TEST000000000000000000TR",
|
threadId: "01TEST000000000000000000TR",
|
||||||
currentRole: { name: "c", systemPrompt: "S" },
|
currentRole: { name: "c", systemPrompt: "S" },
|
||||||
cas,
|
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
role: "a",
|
role: "a",
|
||||||
@@ -149,11 +126,9 @@ describe("buildAgentPrompt", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
const text = await buildAgentPrompt(ctx);
|
const text = await buildAgentPrompt(ctx);
|
||||||
expect(text).not.toContain("HIDDEN_A");
|
|
||||||
expect(text).not.toContain("HIDDEN_B_MIDDLE");
|
|
||||||
expect(text).toContain('Summary: {"n":1}');
|
expect(text).toContain('Summary: {"n":1}');
|
||||||
expect(text).toContain('Summary: {"n":2}');
|
expect(text).toContain('Summary: {"n":2}');
|
||||||
expect(text).toContain("VISIBLE_LAST");
|
expect(text).toContain(`ContentHash: ${hc}`);
|
||||||
expect(text).toContain("## Latest Step: c");
|
expect(text).toContain("## Latest Step: c");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import { getContentMerklePayload } from "@uncaged/workflow";
|
|
||||||
import type { AgentContext } from "@uncaged/workflow-runtime";
|
import type { AgentContext } from "@uncaged/workflow-runtime";
|
||||||
|
|
||||||
async function resolveStepText(ctx: AgentContext, contentHash: string): Promise<string> {
|
|
||||||
const text = await getContentMerklePayload(ctx.cas, contentHash);
|
|
||||||
if (text === null) {
|
|
||||||
throw new Error(`buildAgentPrompt: missing CAS blob for ${contentHash}`);
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Builds the full agent prompt: system instructions plus summarized thread history. */
|
/** Builds the full agent prompt: system instructions plus summarized thread history. */
|
||||||
export async function buildAgentPrompt(ctx: AgentContext): Promise<string> {
|
export async function buildAgentPrompt(ctx: AgentContext): Promise<string> {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
@@ -24,12 +15,10 @@ export async function buildAgentPrompt(ctx: AgentContext): Promise<string> {
|
|||||||
|
|
||||||
if (steps.length === 1) {
|
if (steps.length === 1) {
|
||||||
const s = steps[0];
|
const s = steps[0];
|
||||||
const body = await resolveStepText(ctx, s.contentHash);
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(`## Step: ${s.role}`);
|
lines.push(`## Step: ${s.role}`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(body);
|
lines.push(`ContentHash: ${s.contentHash}`);
|
||||||
lines.push("");
|
|
||||||
lines.push(`Meta: ${JSON.stringify(s.meta)}`);
|
lines.push(`Meta: ${JSON.stringify(s.meta)}`);
|
||||||
} else {
|
} else {
|
||||||
lines.push("");
|
lines.push("");
|
||||||
@@ -41,12 +30,10 @@ export async function buildAgentPrompt(ctx: AgentContext): Promise<string> {
|
|||||||
lines.push(`Summary: ${JSON.stringify(s.meta)}`);
|
lines.push(`Summary: ${JSON.stringify(s.meta)}`);
|
||||||
}
|
}
|
||||||
const last = steps[steps.length - 1];
|
const last = steps[steps.length - 1];
|
||||||
const lastBody = await resolveStepText(ctx, last.contentHash);
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(`## Latest Step: ${last.role}`);
|
lines.push(`## Latest Step: ${last.role}`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(lastBody);
|
lines.push(`ContentHash: ${last.contentHash}`);
|
||||||
lines.push("");
|
|
||||||
lines.push(`Meta: ${JSON.stringify(last.meta)}`);
|
lines.push(`Meta: ${JSON.stringify(last.meta)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ function makeAgentCtx(params: {
|
|||||||
name: "caller",
|
name: "caller",
|
||||||
systemPrompt: "caller",
|
systemPrompt: "caller",
|
||||||
},
|
},
|
||||||
cas: createCasStore(join(params.storageRoot, "agent-ctx-cas")),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import type {
|
|||||||
ThreadContext,
|
ThreadContext,
|
||||||
WorkflowCompletion,
|
WorkflowCompletion,
|
||||||
WorkflowFn,
|
WorkflowFn,
|
||||||
WorkflowRuntime,
|
|
||||||
WorkflowResult,
|
WorkflowResult,
|
||||||
|
WorkflowRuntime,
|
||||||
} from "@uncaged/workflow-runtime";
|
} from "@uncaged/workflow-runtime";
|
||||||
import { START } from "@uncaged/workflow-runtime";
|
import { START } from "@uncaged/workflow-runtime";
|
||||||
import {
|
import {
|
||||||
@@ -24,7 +24,10 @@ import { err, type LogFn, normalizeRefsField, ok, type Result } from "../util/in
|
|||||||
import { runSupervisor } from "./supervisor.js";
|
import { runSupervisor } from "./supervisor.js";
|
||||||
import type { ExecuteThreadIo, ExecuteThreadOptions } from "./types.js";
|
import type { ExecuteThreadIo, ExecuteThreadOptions } from "./types.js";
|
||||||
|
|
||||||
async function resolveEngineRegistryRuntime(storageRoot: string): Promise<
|
async function resolveEngineRegistryRuntime(
|
||||||
|
storageRoot: string,
|
||||||
|
cas: CasStore,
|
||||||
|
): Promise<
|
||||||
Result<
|
Result<
|
||||||
{
|
{
|
||||||
extract: ReturnType<typeof createExtract>;
|
extract: ReturnType<typeof createExtract>;
|
||||||
@@ -51,7 +54,7 @@ async function resolveEngineRegistryRuntime(storageRoot: string): Promise<
|
|||||||
apiKey: ex.apiKey,
|
apiKey: ex.apiKey,
|
||||||
model: ex.model,
|
model: ex.model,
|
||||||
};
|
};
|
||||||
return ok({ extract: createExtract(llmProvider), workflowConfig: cfg });
|
return ok({ extract: createExtract(llmProvider, { cas }), workflowConfig: cfg });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function appendDataLine(path: string, record: unknown): Promise<void> {
|
async function appendDataLine(path: string, record: unknown): Promise<void> {
|
||||||
@@ -368,7 +371,7 @@ export async function executeThread(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const registryRuntime = await resolveEngineRegistryRuntime(options.storageRoot);
|
const registryRuntime = await resolveEngineRegistryRuntime(options.storageRoot, io.cas);
|
||||||
if (!registryRuntime.ok) {
|
if (!registryRuntime.ok) {
|
||||||
throw new Error(registryRuntime.error);
|
throw new Error(registryRuntime.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import type { ExtractContext, ExtractFn, LlmProvider } from "@uncaged/workflow-runtime";
|
import type { ExtractContext, ExtractFn, LlmProvider } from "@uncaged/workflow-runtime";
|
||||||
import type * as z from "zod/v4";
|
import type * as z from "zod/v4";
|
||||||
import { getContentMerklePayload } from "../cas/index.js";
|
import { type CasStore, getContentMerklePayload } from "../cas/index.js";
|
||||||
import { reactExtract } from "./react-extract.js";
|
import { reactExtract } from "./react-extract.js";
|
||||||
|
|
||||||
|
export type ExtractDeps = {
|
||||||
|
cas: CasStore;
|
||||||
|
};
|
||||||
|
|
||||||
/** Builds the user-side extraction prompt (thread + agent output + instruction). */
|
/** Builds the user-side extraction prompt (thread + agent output + instruction). */
|
||||||
export async function buildExtractUserContent(
|
export async function buildExtractUserContent(
|
||||||
ctx: ExtractContext,
|
ctx: ExtractContext,
|
||||||
prompt: string,
|
prompt: string,
|
||||||
|
deps: ExtractDeps,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(`## Role: ${ctx.currentRole.name}`);
|
lines.push(`## Role: ${ctx.currentRole.name}`);
|
||||||
@@ -18,7 +23,7 @@ export async function buildExtractUserContent(
|
|||||||
if (ctx.steps.length > 0) {
|
if (ctx.steps.length > 0) {
|
||||||
lines.push("## Thread History");
|
lines.push("## Thread History");
|
||||||
for (const step of ctx.steps) {
|
for (const step of ctx.steps) {
|
||||||
const body = await getContentMerklePayload(ctx.cas, step.contentHash);
|
const body = await getContentMerklePayload(deps.cas, step.contentHash);
|
||||||
if (body === null) {
|
if (body === null) {
|
||||||
throw new Error(`extract: missing CAS blob for step ${step.role}: ${step.contentHash}`);
|
throw new Error(`extract: missing CAS blob for step ${step.role}: ${step.contentHash}`);
|
||||||
}
|
}
|
||||||
@@ -44,14 +49,14 @@ export async function buildExtractUserContent(
|
|||||||
* Merkle DAG and a schema-shaped `extract` tool); the loop also accepts a plain-JSON
|
* Merkle DAG and a schema-shaped `extract` tool); the loop also accepts a plain-JSON
|
||||||
* assistant reply as a short-circuit, which covers the legacy "single" extraction path.
|
* assistant reply as a short-circuit, which covers the legacy "single" extraction path.
|
||||||
*/
|
*/
|
||||||
export function createExtract(provider: LlmProvider): ExtractFn {
|
export function createExtract(provider: LlmProvider, deps: ExtractDeps): ExtractFn {
|
||||||
return async <T extends Record<string, unknown>>(
|
return async <T extends Record<string, unknown>>(
|
||||||
schema: z.ZodType<T>,
|
schema: z.ZodType<T>,
|
||||||
prompt: string,
|
prompt: string,
|
||||||
ctx: ExtractContext,
|
ctx: ExtractContext,
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
const text = await buildExtractUserContent(ctx, prompt);
|
const text = await buildExtractUserContent(ctx, prompt, deps);
|
||||||
const result = await reactExtract({ text, schema, provider, cas: ctx.cas });
|
const result = await reactExtract({ text, schema, provider, cas: deps.cas });
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new Error(`extract failed: ${result.error}`);
|
throw new Error(`extract failed: ${result.error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"files": [],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
|
|||||||
Reference in New Issue
Block a user