feat(chat): stream paseo tool calls
This commit is contained in:
@@ -26,7 +26,7 @@ FLUE_DB_TOKEN=replace-with-a-long-random-token
|
|||||||
|
|
||||||
# Agent model provider
|
# Agent model provider
|
||||||
AGENT_MODEL_PROVIDER=cheaptricks
|
AGENT_MODEL_PROVIDER=cheaptricks
|
||||||
AGENT_MODEL_NAME=minimax-m3
|
AGENT_MODEL_NAME=glm-5.2
|
||||||
AGENT_MODEL_API=openai-completions
|
AGENT_MODEL_API=openai-completions
|
||||||
AGENT_MODEL_BASE_URL=https://ai.example.com/v1
|
AGENT_MODEL_BASE_URL=https://ai.example.com/v1
|
||||||
AGENT_MODEL_API_KEY=replace-with-provider-api-key
|
AGENT_MODEL_API_KEY=replace-with-provider-api-key
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import { getMessageText, isMessageStreaming } from "@/lib/chat/transforms";
|
|||||||
import type { ChatMessageProps } from "@/lib/chat/types";
|
import type { ChatMessageProps } from "@/lib/chat/types";
|
||||||
|
|
||||||
import { AssistantIdentity } from "./assistant-identity";
|
import { AssistantIdentity } from "./assistant-identity";
|
||||||
|
import { ChatToolCall } from "./chat-tool-call";
|
||||||
|
|
||||||
export const ChatMessage = ({ message }: ChatMessageProps) => {
|
export const ChatMessage = ({ message }: ChatMessageProps) => {
|
||||||
const isUser = message.role === "user";
|
const isUser = message.role === "user";
|
||||||
const isStreaming = isMessageStreaming(message);
|
const isStreaming = isMessageStreaming(message);
|
||||||
const text = getMessageText(message);
|
const text = getMessageText(message);
|
||||||
|
|
||||||
if (!text && !isStreaming) {
|
if (message.parts.length === 0 && !isStreaming) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +64,11 @@ export const ChatMessage = ({ message }: ChatMessageProps) => {
|
|||||||
: "w-full max-w-none overflow-visible leading-7 text-foreground/95"
|
: "w-full max-w-none overflow-visible leading-7 text-foreground/95"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{message.parts.map((part) =>
|
||||||
|
part.type === "dynamic-tool" ? (
|
||||||
|
<ChatToolCall key={part.toolCallId} part={part} />
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
{content}
|
{content}
|
||||||
</BubbleContent>
|
</BubbleContent>
|
||||||
</Bubble>
|
</Bubble>
|
||||||
|
|||||||
41
apps/web/src/components/chat/chat-tool-call.tsx
Normal file
41
apps/web/src/components/chat/chat-tool-call.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Check, LoaderCircle, Terminal, TriangleAlert } from "lucide-react";
|
||||||
|
|
||||||
|
import type { ChatToolCallProps } from "@/lib/chat/types";
|
||||||
|
|
||||||
|
export const ChatToolCall = ({ part }: ChatToolCallProps) => {
|
||||||
|
let detail =
|
||||||
|
typeof part.input === "string"
|
||||||
|
? part.input
|
||||||
|
: JSON.stringify(part.input, null, 2);
|
||||||
|
let Icon = LoaderCircle;
|
||||||
|
let status = "Running";
|
||||||
|
if (part.state === "output-available") {
|
||||||
|
detail =
|
||||||
|
typeof part.output === "string"
|
||||||
|
? part.output
|
||||||
|
: JSON.stringify(part.output, null, 2);
|
||||||
|
Icon = Check;
|
||||||
|
status = "Completed";
|
||||||
|
} else if (part.state === "output-error") {
|
||||||
|
detail = part.errorText;
|
||||||
|
Icon = TriangleAlert;
|
||||||
|
status = "Failed";
|
||||||
|
}
|
||||||
|
const isRunning = part.state === "input-available";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details className="group/tool overflow-hidden rounded-xl border border-border/60 bg-card/45 text-xs">
|
||||||
|
<summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-muted-foreground marker:hidden">
|
||||||
|
<Terminal className="size-3.5 shrink-0" />
|
||||||
|
<span className="min-w-0 flex-1 truncate font-medium text-foreground/85">
|
||||||
|
{part.toolName}
|
||||||
|
</span>
|
||||||
|
<span>{status}</span>
|
||||||
|
<Icon className={`size-3.5 ${isRunning ? "animate-spin" : ""}`} />
|
||||||
|
</summary>
|
||||||
|
<pre className="max-h-72 overflow-auto border-t border-border/50 bg-background/60 px-3 py-2 font-mono text-[11px] leading-5 whitespace-pre-wrap text-muted-foreground">
|
||||||
|
{detail}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ export const CHAT_AGENT = {
|
|||||||
name: "zopu",
|
name: "zopu",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const MODEL_LABEL = "MiniMax M3";
|
export const MODEL_LABEL = "GLM-5.2";
|
||||||
|
|
||||||
export const SUGGESTIONS = [
|
export const SUGGESTIONS = [
|
||||||
["Think it through", "Help me make a difficult decision"],
|
["Think it through", "Help me make a difficult decision"],
|
||||||
|
|||||||
@@ -24,11 +24,15 @@ export const getMessageText = (message: FlueConversationMessage): string =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const isMessageStreaming = (message: FlueConversationMessage): boolean =>
|
export const isMessageStreaming = (message: FlueConversationMessage): boolean =>
|
||||||
message.parts.some(
|
message.parts.some((part) => {
|
||||||
(part) =>
|
if (part.type === "dynamic-tool") {
|
||||||
|
return part.state === "input-available";
|
||||||
|
}
|
||||||
|
return (
|
||||||
(part.type === "text" || part.type === "reasoning") &&
|
(part.type === "text" || part.type === "reasoning") &&
|
||||||
part.state === "streaming"
|
part.state === "streaming"
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const getStatusDotClass = (status: AgentStatus): string => {
|
export const getStatusDotClass = (status: AgentStatus): string => {
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { AgentStatus, FlueConversationMessage } from "@flue/react";
|
import type {
|
||||||
|
AgentStatus,
|
||||||
|
FlueConversationMessage,
|
||||||
|
FlueConversationPart,
|
||||||
|
} from "@flue/react";
|
||||||
|
|
||||||
export interface ChatAgentState {
|
export interface ChatAgentState {
|
||||||
error?: Error;
|
error?: Error;
|
||||||
@@ -29,4 +33,8 @@ export interface ChatMessageProps {
|
|||||||
message: FlueConversationMessage;
|
message: FlueConversationMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatToolCallProps {
|
||||||
|
part: Extract<FlueConversationPart, { type: "dynamic-tool" }>;
|
||||||
|
}
|
||||||
|
|
||||||
export type AssistantResponseState = "thinking" | "writing";
|
export type AssistantResponseState = "thinking" | "writing";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { defineAgent } from "@flue/runtime";
|
|||||||
import type { AgentRouteHandler } from "@flue/runtime";
|
import type { AgentRouteHandler } from "@flue/runtime";
|
||||||
import { local } from "@flue/runtime/node";
|
import { local } from "@flue/runtime/node";
|
||||||
|
|
||||||
|
import paseo from "../skills/paseo/SKILL.md" with { type: "skill" };
|
||||||
import { paseoCli } from "../tools/paseo";
|
import { paseoCli } from "../tools/paseo";
|
||||||
|
|
||||||
export const route: AgentRouteHandler = (_context, next) => next();
|
export const route: AgentRouteHandler = (_context, next) => next();
|
||||||
@@ -16,6 +17,7 @@ export default defineAgent(({ env }) => {
|
|||||||
"You are Zopu, a concise and helpful assistant. Answer the user's request directly. Ask a clarifying question only when the request cannot be completed without one.",
|
"You are Zopu, a concise and helpful assistant. Answer the user's request directly. Ask a clarifying question only when the request cannot be completed without one.",
|
||||||
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
|
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
|
||||||
sandbox: local({ cwd: "/workspace/code" }),
|
sandbox: local({ cwd: "/workspace/code" }),
|
||||||
|
skills: [paseo],
|
||||||
tools: [paseoCli],
|
tools: [paseoCli],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
143
packages/agents/src/skills/paseo/SKILL.md
Normal file
143
packages/agents/src/skills/paseo/SKILL.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
name: paseo
|
||||||
|
description: Paseo reference for managing workspaces, agents, schedules, and heartbeats.
|
||||||
|
---
|
||||||
|
|
||||||
|
Paseo is a daemon that supervises AI coding agents on your machine. Control it through tools or a CLI.
|
||||||
|
|
||||||
|
## Workspaces
|
||||||
|
|
||||||
|
**`create_workspace`** — create a workspace independently of any agent. Required: `isolation` (`local` or `worktree`). Worktree isolation supports `mode: "branch-off" | "checkout-branch" | "checkout-pr"`: use `branchName`/`baseBranch` for a new branch, `branch` for an existing branch, or `prNumber` plus optional `forge`/`projectPath` for a change request. `worktreeSlug` controls the managed path. Returns the workspace descriptor centered on `workspaceId`.
|
||||||
|
|
||||||
|
**`list_workspaces`** — list active workspaces.
|
||||||
|
|
||||||
|
**`archive_workspace`** — `{ workspaceId }`. Archives the workspace, its agents, and its terminals. Local directories remain; Paseo removes an owned worktree only after its final active workspace reference is archived.
|
||||||
|
|
||||||
|
Worktree creation and reference accounting are implementation details of `isolation: "worktree"`.
|
||||||
|
|
||||||
|
## Agents
|
||||||
|
|
||||||
|
**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Optional: `workspaceId`, `notifyOnFinish`, `settings`, `labels`. Returns `{ agentId, workspaceId, … }`.
|
||||||
|
|
||||||
|
Initial runtime settings live under `settings`: `modeId`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }` when creating the agent.
|
||||||
|
|
||||||
|
Agent-scoped creation always creates your subagent. Omit `workspaceId` to use your current workspace; pass a workspace returned by `create_workspace` for isolated delegation. Placement never changes parentage.
|
||||||
|
|
||||||
|
Detach is an explicit user action in the subagents track, not an agent tool. A cross-workspace child remains your subagent even though it also appears as a normal tab in its workspace.
|
||||||
|
|
||||||
|
Agent-scoped `create_agent` defaults `notifyOnFinish` to true. Set it to `false` only for truly fire-and-forget agents.
|
||||||
|
|
||||||
|
**`send_agent_prompt`** — `{ agentId, prompt }`. Use for follow-ups to an existing agent. Agent-scoped prompt calls default to `background: true` and `notifyOnFinish: true`; top-level calls default to blocking with no callback. For a synchronous follow-up, pass `background: false` and use the returned result.
|
||||||
|
|
||||||
|
**`update_agent`** — `{ agentId, name?, labels?, settings? }`. Use `settings` for runtime changes on an existing agent: `modeId`, `model`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }`.
|
||||||
|
|
||||||
|
**`list_agents`** — filter by `cwd`, `statuses`, `sinceHours`, `includeArchived`.
|
||||||
|
|
||||||
|
**`archive_agent`** — `{ agentId }`. Interrupts if running, removes from active list.
|
||||||
|
|
||||||
|
## Provider discovery
|
||||||
|
|
||||||
|
**`list_providers`** — compact provider availability and modes.
|
||||||
|
|
||||||
|
**`list_models`** — full model list for one provider. Use only when you need model IDs or thinking options; the list can be large.
|
||||||
|
|
||||||
|
**`inspect_provider`** — compact provider capability and feature inspection. Required: `provider`; pass `cwd` when you are not in an agent-scoped session. Optional: `settings` with draft `model`, `modeId`, `thinkingOptionId`, and `features`.
|
||||||
|
|
||||||
|
Only set feature IDs returned by `inspect_provider`. For Codex fast mode, look for `fast_mode` and pass `settings: { features: { "fast_mode": true } }` to `create_agent` or `update_agent`.
|
||||||
|
|
||||||
|
## Schedules and heartbeats
|
||||||
|
|
||||||
|
**`create_schedule`** — starts a new agent on a cron cadence. Required: `prompt`, `cron`, `provider`. Optional: `timezone`, `name`, `cwd`, `maxRuns`, `expiresIn`. Use when recurring work should live in fresh agents.
|
||||||
|
|
||||||
|
**`create_heartbeat`** — sends you a prompt on a cron cadence. Required: `prompt`, `cron`. Optional: `timezone`, `name`, `maxRuns`, `expiresIn`. Use for reminders, PR/build babysitting, and status checks that should return to this conversation.
|
||||||
|
|
||||||
|
**`delete_heartbeat`** stops it. MCP intentionally exposes no heartbeat update tool; delete and recreate when its task or cadence changes.
|
||||||
|
|
||||||
|
Schedules have the full list/inspect/update/pause/resume/run-once/log/delete surface. Heartbeats deliberately do not.
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
`claude/sonnet` (default), `claude/opus` (harder reasoning), `codex/gpt-5.4` (frontier coding), `claude/haiku` (tests only).
|
||||||
|
|
||||||
|
## Orchestration preferences
|
||||||
|
|
||||||
|
User-specific configuration at `~/.paseo/orchestration-preferences.json`. **Before any Paseo skill chooses a provider or creates an agent, it must read this file.** Reading means an actual file read, not relying on these examples or defaults. Never hardcode a provider string in another skill — resolve through this file.
|
||||||
|
|
||||||
|
Two parts:
|
||||||
|
|
||||||
|
- `providers` — map of role categories to provider strings. Pass straight to `create_agent`'s `provider` field.
|
||||||
|
- `preferences` — freeform string array. Read on startup; weave into agent prompts contextually.
|
||||||
|
|
||||||
|
Categories: `impl`, `ui`, `research`, `planning`, `audit`. Skills pick the category that matches the role they're launching.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"impl": "codex/gpt-5.4",
|
||||||
|
"ui": "claude/opus",
|
||||||
|
"research": "codex/gpt-5.4",
|
||||||
|
"planning": "codex/gpt-5.4",
|
||||||
|
"audit": "codex/gpt-5.4"
|
||||||
|
},
|
||||||
|
"preferences": [
|
||||||
|
"Claude Opus is the right choice for anything artistic or human-skill-oriented: copywriting, naming, UX copy, visual design, styling. Codex is the workhorse for mechanical work."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the file is missing, use sensible defaults and tell the user once.
|
||||||
|
|
||||||
|
## Waiting
|
||||||
|
|
||||||
|
Agents take time — 10–30+ minutes is routine. Favor asynchronous workflows.
|
||||||
|
|
||||||
|
For agent-scoped `create_agent` and background `send_agent_prompt`, leave `notifyOnFinish` omitted or set it to `true` unless the work is truly fire-and-forget. You will get notified when the target agent finishes, errors, or needs permission. Move on to other work. The notification arrives on its own.
|
||||||
|
|
||||||
|
Don't poll `list_agents` or `get_agent_status` to "check on" a running agent. The notification will tell you.
|
||||||
|
|
||||||
|
## CLI semantics
|
||||||
|
|
||||||
|
The CLI and tools use the same ownership semantics even where their syntax differs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
paseo workspace create --isolation worktree --mode branch-off --new-branch fix-x --base main
|
||||||
|
paseo workspace create --isolation worktree --mode checkout-branch --branch existing-work
|
||||||
|
paseo workspace create --isolation worktree --mode checkout-pr --pr-number 42
|
||||||
|
paseo run --provider codex/gpt-5.4 --mode full-access --workspace <workspace-id> "<prompt>"
|
||||||
|
paseo send <agent-id> "<follow-up>"
|
||||||
|
paseo ls
|
||||||
|
paseo schedule create --cron "*/15 * * * *" "ping main build"
|
||||||
|
paseo heartbeat create --cron "*/15 * * * *" "check the build"
|
||||||
|
```
|
||||||
|
|
||||||
|
Discover with `paseo --help` and `paseo <cmd> --help`.
|
||||||
|
|
||||||
|
**If `paseo` isn't on PATH but the desktop app is installed**, the bundled CLI is at:
|
||||||
|
|
||||||
|
- macOS: `/Applications/Paseo.app/Contents/Resources/bin/paseo`
|
||||||
|
- Linux: `<install-dir>/resources/bin/paseo`
|
||||||
|
- Windows: `C:\Program Files\Paseo\resources\bin\paseo.cmd`
|
||||||
|
|
||||||
|
The desktop app's first-run hook (`installCli`) symlinks this to `~/.local/bin/paseo` (macOS/Linux) or drops a `.cmd` trampoline (Windows) and adds `~/.local/bin` to PATH via shell rc files. If that didn't take, offer to symlink it — don't do it silently.
|
||||||
|
|
||||||
|
## Ops and debugging
|
||||||
|
|
||||||
|
Daemon-client architecture: the daemon owns agent lifecycle, state, and the WebSocket API. Tools, CLI, mobile, and desktop apps are all clients.
|
||||||
|
|
||||||
|
| | Default |
|
||||||
|
| --- | --- |
|
||||||
|
| Listen address | `127.0.0.1:6767` (override `PASEO_LISTEN`) |
|
||||||
|
| Home | `~/.paseo` (override `PASEO_HOME`) |
|
||||||
|
| Daemon log | `$PASEO_HOME/daemon.log` |
|
||||||
|
| Agent state | `$PASEO_HOME/agents/<id>.json` |
|
||||||
|
| Worktrees | `$PASEO_HOME/worktrees/` (or `worktrees.root` in `config.json`) |
|
||||||
|
| PID file | `$PASEO_HOME/paseo.pid` |
|
||||||
|
| Health | `GET http://127.0.0.1:6767/api/health` |
|
||||||
|
|
||||||
|
Debug order:
|
||||||
|
|
||||||
|
1. `tail -n 200 ~/.paseo/daemon.log`.
|
||||||
|
2. `paseo daemon status` for liveness.
|
||||||
|
3. `curl -s localhost:6767/api/health` if the CLI itself is suspect.
|
||||||
|
|
||||||
|
**Never restart the daemon without explicit user approval** — it kills every running agent, including, often, the one asking.
|
||||||
Reference in New Issue
Block a user