Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/temporal-node-activity-heartbeat-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflowbuilder/temporal': minor
---

`ActivityProfile` (and `nodeActivityProfiles`) gains an optional `heartbeatTimeout` key for long-running activities that need Temporal to detect a stalled or crashed worker faster than `startToCloseTimeout` alone allows.
27 changes: 27 additions & 0 deletions apps/ai-studio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,30 @@ This is a sibling to `apps/demo`, not a layer over it. They share the SDK; nothi
| Backend | None (pure SPA) | Required (Hono + Temporal) |
| Plugin model | Plugins decorate the editor | Direct JSX composition; one slim plugin for node markers |
| Dev port | 4200 | 4201 |

## Agent Harness node

`Agent Harness` (`ai-studio/agent-harness`) delegates a workflow step to an external
autonomous coding-agent CLI (GitHub Copilot in v1) instead of a single bounded LLM call.
Its property panel has four accordion sections: **General** (label, prompt, provider),
**Execution** (model, effort, context, idle timeout), **Tools** (tools preset,
allowed/denied tool lists), and **Advanced** (output format, MCP config, skills,
sub-agents, max budget, mutates-checkout, persist-session).

**Functional in v1:** `prompt`, `provider` (`copilot` only), `model`, `effort`,
`idle_timeout`, `mutatesCheckout`, `agents`.

**Rendered but NOT YET SUPPORTED in v1** (backend deferred, but per issue #147's "fields
must still render" requirement they are intentionally present rather than hidden —
this is not an oversight): `context` beyond `'fresh'` (`'shared'`/`'resume'` need session
persistence), `output_format` (no structured-output enforcement yet), `mcp`, `skills`,
`maxBudgetUsd`, `persistSession`. Each of these fields' label/placeholder in the panel
says "not yet supported" so this is visible in the UI itself, not just in this doc.

**`idle_timeout` is in milliseconds, not seconds.** `300` means 300ms; for a 5-minute
timeout set `idle_timeout: 300000`.

**Demo template:** "Agent Harness Demo" (`data/agent-harness-flow.ts`) — a
`trigger` → `agent-harness` → `visualize` chain that asks the agent to write a
`plan.md` outlining rate-limiting approaches and summarize the tradeoffs, exercising
both file tools and a returned text result.
100 changes: 100 additions & 0 deletions apps/ai-studio/src/data/agent-harness-flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { DiagramModel, TemplateModel } from '@workflowbuilder/sdk';

const diagram: DiagramModel = {
name: 'Agent Harness Demo',
diagram: {
nodes: [
{
id: 'trigger-1',
type: 'start-node',
position: { x: 0, y: 300 },
data: {
segments: [],
isStartNode: true,
properties: {
label: 'Start',
description: 'Kicks off the agent harness demo.',
inputPrompt: `Create a file plan.md outlining three approaches to rate-limiting an HTTP API, then summarize the tradeoffs.`,
},
type: 'ai-studio/trigger',
icon: 'Lightning',
},
selected: false,
measured: { width: 258, height: 63 },
dragging: false,
},
{
id: 'agent-harness-1',
type: 'node',
position: { x: 380, y: 300 },
data: {
segments: [],
properties: {
label: 'Agent Harness',
description: 'Delegates the task to Copilot via the agent harness CLI.',
prompt: `Create a file plan.md outlining three approaches to rate-limiting an HTTP API, then summarize the tradeoffs.`,
provider: 'copilot',
model: 'auto',
context: 'fresh',
toolsMode: 'all',
mutatesCheckout: false,
persistSession: false,
idle_timeout: 300_000,
},
type: 'ai-studio/agent-harness',
icon: 'Terminal',
},
selected: false,
measured: { width: 258, height: 123 },
dragging: false,
},
{
id: 'visualize-1',
type: 'node',
position: { x: 760, y: 300 },
data: {
segments: [],
properties: {
label: 'Visualize',
description: 'Renders the agent output (auto-detects the format).',
mode: 'auto',
},
type: 'ai-studio/visualize',
icon: 'Eye',
},
selected: false,
measured: { width: 258, height: 123 },
dragging: false,
},
],
edges: [
{
source: 'trigger-1',
sourceHandle: 'source',
target: 'agent-harness-1',
targetHandle: 'target',
type: 'labelEdge',
id: 'edge-trigger-agent-harness',
data: {},
},
{
source: 'agent-harness-1',
sourceHandle: 'source',
target: 'visualize-1',
targetHandle: 'target',
type: 'labelEdge',
id: 'edge-agent-harness-visualize',
data: {},
},
],
viewport: { x: 180, y: 150, zoom: 0.7 },
},
layoutDirection: 'RIGHT',
};

export const agentHarnessFlow: TemplateModel = {
id: 306,
name: 'Agent Harness Demo',
value: diagram,
icon: 'Terminal',
};
2 changes: 2 additions & 0 deletions apps/ai-studio/src/data/ai-studio-templates.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TemplateModel } from '@workflowbuilder/sdk';

import { agentHarnessFlow } from './agent-harness-flow';
import { aiDebateFlow } from './ai-debate-flow';
import { contentRepurposerFlow } from './content-repurposer-flow';
import { meetingNotesFlow } from './meeting-notes-flow';
Expand All @@ -12,4 +13,5 @@ export const aiStudioTemplates: TemplateModel[] = [
contentRepurposerFlow,
meetingNotesFlow,
researchFlow,
agentHarnessFlow,
];
9 changes: 8 additions & 1 deletion apps/ai-studio/src/data/node-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { PaletteItemOrGroup } from '@workflowbuilder/sdk';

import { agentHarnessPaletteItem } from '../nodes/agent-harness';
import { aiAgentPaletteItem } from '../nodes/ai-agent';
import { decisionPaletteItem } from '../nodes/decision';
import { triggerPaletteItem } from '../nodes/trigger';
Expand All @@ -9,6 +10,12 @@ export const aiStudioNodeTypes: PaletteItemOrGroup[] = [
{
label: 'AI Studio',
isOpen: true,
groupItems: [triggerPaletteItem, aiAgentPaletteItem, decisionPaletteItem, visualizePaletteItem],
groupItems: [
triggerPaletteItem,
aiAgentPaletteItem,
agentHarnessPaletteItem,
decisionPaletteItem,
visualizePaletteItem,
],
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { NodeDataProperties } from '@workflowbuilder/sdk';

import type { AgentHarnessSchema } from './schema';

export const defaultPropertiesData: NodeDataProperties<AgentHarnessSchema> = {
label: 'Agent Harness',
description: '',
prompt: '',
provider: 'copilot',
model: 'auto',
context: 'fresh',
toolsMode: 'none',
mutatesCheckout: false,
persistSession: false,
};
24 changes: 24 additions & 0 deletions apps/ai-studio/src/nodes/agent-harness/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NodeType, type PaletteItem } from '@workflowbuilder/sdk';

import { defaultPropertiesData } from './default-properties-data';
import { type AgentHarnessSchema, schema } from './schema';
import { uischema } from './uischema';

export const agentHarnessPaletteItem: PaletteItem<AgentHarnessSchema> = {
label: 'Agent Harness',
description: 'Delegate a step to an autonomous coding-agent CLI (GitHub Copilot)',
type: 'ai-studio/agent-harness',
icon: 'Terminal',
templateType: NodeType.Node,
defaultPropertiesData,
schema,
uischema,
// Lets `{{ nodes.<id>.response }}` references resolve to a real mention instead of a "missing mention" pill.
outputSchema: {
type: 'default',
properties: {
response: { type: 'string', label: 'Response', description: 'The text produced by the agent run' },
tokens: { type: 'object', label: 'Tokens', description: 'Token usage reported by the provider' },
},
},
};
94 changes: 94 additions & 0 deletions apps/ai-studio/src/nodes/agent-harness/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { sharedProperties } from '@workflowbuilder/sdk';
import type { NodeSchema } from '@workflowbuilder/sdk';

// Only Copilot is functionally wired in v1 (see `apps/execution-worker/src/agent-harness/registry.ts`).
// Kept as a single-entry option list, rather than a plain string, so the Select still renders — a
// second provider is a one-line addition here once its backend lands (per issue #147 parity intent).
const providerOptions = [{ label: 'GitHub Copilot', value: 'copilot' }];

// Copilot's own effort ladder (`COPILOT_EFFORTS` in providers/copilot/config.ts) is narrower than the
// full cross-provider `EffortRung` union — only offer the rungs this provider actually honors.
const effortOptions = [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
{ label: 'Extra high', value: 'xhigh' },
];

// 'shared'/'resume' are NOT YET SUPPORTED (session persistence is out of scope for v1, see the
// implementation plan §2) — rendered anyway per issue #147 ("fields must still render").
const contextOptions = [
{ label: 'Fresh (new session)', value: 'fresh' },
{ label: 'Shared (not yet supported)', value: 'shared' },
{ label: 'Resume (not yet supported)', value: 'resume' },
];

// UI-only convenience preset; expanding it into allowedTools/deniedTools is a manual step for now
// (no automatic preset -> field wiring in this milestone, see nodes/agent-harness/index.ts handoff).
const toolsModeOptions = [
{ label: 'None', value: 'none' },
{ label: 'Read-only', value: 'read-only' },
{ label: 'Edit-only', value: 'edit-only' },
{ label: 'All tools', value: 'all' },
];

export const schema = {
type: 'object',
properties: {
...sharedProperties,
prompt: {
type: 'string',
},
provider: {
type: 'string',
options: providerOptions,
},
model: {
type: 'string',
},
effort: {
type: 'string',
options: effortOptions,
},
context: {
type: 'string',
options: contextOptions,
},
idle_timeout: {
type: 'number',
},
toolsMode: {
type: 'string',
options: toolsModeOptions,
},
allowedTools: {
type: 'string',
},
deniedTools: {
type: 'string',
},
output_format: {
type: 'string',
},
mcp: {
type: 'string',
},
skills: {
type: 'string',
},
agents: {
type: 'string',
},
maxBudgetUsd: {
type: 'number',
},
mutatesCheckout: {
type: 'boolean',
},
persistSession: {
type: 'boolean',
},
},
} satisfies NodeSchema;

export type AgentHarnessSchema = typeof schema;
Loading