feat: improve agent stream view UI, fix history replay timestamps, and enhance planner/worker loop
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"packages/website"
|
"packages/website"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run dev --workspace=@paseo/server",
|
"dev": "NODE_ENV=development tsx packages/server/scripts/dev-runner.ts",
|
||||||
"dev:app": "npm run start --workspace=@paseo/app",
|
"dev:app": "npm run start --workspace=@paseo/app",
|
||||||
"dev:website": "npm run dev --workspace=@paseo/website",
|
"dev:website": "npm run dev --workspace=@paseo/website",
|
||||||
"build": "npm run build --workspaces --if-present",
|
"build": "npm run build --workspaces --if-present",
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
import { View, Text, Pressable, Modal, RefreshControl, FlatList, type ListRenderItem } from "react-native";
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
Pressable,
|
||||||
|
Modal,
|
||||||
|
RefreshControl,
|
||||||
|
FlatList,
|
||||||
|
type ListRenderItem,
|
||||||
|
} from "react-native";
|
||||||
import { useCallback, useState, type ReactElement } from "react";
|
import { useCallback, useState, type ReactElement } from "react";
|
||||||
import { router, usePathname } from "expo-router";
|
import { router, usePathname } from "expo-router";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { formatTimeAgo } from "@/utils/time";
|
import { formatTimeAgo } from "@/utils/time";
|
||||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigation-timing";
|
import {
|
||||||
|
buildAgentNavigationKey,
|
||||||
|
startNavigationTiming,
|
||||||
|
} from "@/utils/navigation-timing";
|
||||||
|
|
||||||
interface AgentListProps {
|
interface AgentListProps {
|
||||||
agents: AggregatedAgent[];
|
agents: AggregatedAgent[];
|
||||||
@@ -30,7 +41,9 @@ export function AgentList({
|
|||||||
|
|
||||||
// Get the methods for the specific server
|
// Get the methods for the specific server
|
||||||
const methods = useSessionStore((state) =>
|
const methods = useSessionStore((state) =>
|
||||||
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined
|
actionAgent?.serverId
|
||||||
|
? state.sessions[actionAgent.serverId]?.methods
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
const deleteAgent = methods?.deleteAgent;
|
const deleteAgent = methods?.deleteAgent;
|
||||||
|
|
||||||
@@ -94,7 +107,11 @@ export function AgentList({
|
|||||||
const isRunning = agent.status === "running";
|
const isRunning = agent.status === "running";
|
||||||
const agentKey = `${agent.serverId}:${agent.id}`;
|
const agentKey = `${agent.serverId}:${agent.id}`;
|
||||||
const isSelected = selectedAgentId === agentKey;
|
const isSelected = selectedAgentId === agentKey;
|
||||||
const statusColor = isRunning ? theme.colors.palette.blue[500] : agent.requiresAttention ? theme.colors.success : null;
|
const statusColor = isRunning
|
||||||
|
? theme.colors.palette.blue[500]
|
||||||
|
: agent.requiresAttention
|
||||||
|
? theme.colors.success
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -108,26 +125,28 @@ export function AgentList({
|
|||||||
onLongPress={() => handleAgentLongPress(agent)}
|
onLongPress={() => handleAgentLongPress(agent)}
|
||||||
>
|
>
|
||||||
{({ hovered }) => (
|
{({ hovered }) => (
|
||||||
<View style={styles.agentContent}>
|
<View style={styles.agentContent}>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
{statusColor && (
|
{statusColor && (
|
||||||
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
<View
|
||||||
)}
|
style={[styles.statusDot, { backgroundColor: statusColor }]}
|
||||||
<Text
|
/>
|
||||||
style={[
|
)}
|
||||||
styles.agentTitle,
|
<Text
|
||||||
(isSelected || hovered) && styles.agentTitleHighlighted,
|
style={[
|
||||||
]}
|
styles.agentTitle,
|
||||||
numberOfLines={1}
|
(isSelected || hovered) && styles.agentTitleHighlighted,
|
||||||
>
|
]}
|
||||||
{agent.title || "New Agent"}
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{agent.title || "New Agent"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.secondaryRow} numberOfLines={1}>
|
||||||
|
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.secondaryRow} numberOfLines={1}>
|
|
||||||
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
@@ -176,7 +195,10 @@ export function AgentList({
|
|||||||
onRequestClose={handleCloseActionSheet}
|
onRequestClose={handleCloseActionSheet}
|
||||||
>
|
>
|
||||||
<View style={styles.sheetOverlay}>
|
<View style={styles.sheetOverlay}>
|
||||||
<Pressable style={styles.sheetBackdrop} onPress={handleCloseActionSheet} />
|
<Pressable
|
||||||
|
style={styles.sheetBackdrop}
|
||||||
|
onPress={handleCloseActionSheet}
|
||||||
|
/>
|
||||||
<View style={styles.sheetContainer}>
|
<View style={styles.sheetContainer}>
|
||||||
<View style={styles.sheetHandle} />
|
<View style={styles.sheetHandle} />
|
||||||
<Text style={styles.sheetTitle}>
|
<Text style={styles.sheetTitle}>
|
||||||
@@ -195,7 +217,8 @@ export function AgentList({
|
|||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.sheetDeleteText,
|
styles.sheetDeleteText,
|
||||||
(!deleteAgent || isActionDaemonUnavailable) && styles.sheetDeleteTextDisabled,
|
(!deleteAgent || isActionDaemonUnavailable) &&
|
||||||
|
styles.sheetDeleteTextDisabled,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isActionDaemonUnavailable ? "Host offline" : "Delete agent"}
|
{isActionDaemonUnavailable ? "Host offline" : "Delete agent"}
|
||||||
@@ -246,7 +269,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
row: {
|
row: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[0],
|
||||||
},
|
},
|
||||||
agentTitle: {
|
agentTitle: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -255,10 +278,12 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
xs: "400",
|
xs: "400",
|
||||||
md: "300",
|
md: "300",
|
||||||
},
|
},
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foreground,
|
||||||
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
agentTitleHighlighted: {
|
agentTitleHighlighted: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
|
opacity: 1,
|
||||||
},
|
},
|
||||||
secondaryRow: {
|
secondaryRow: {
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
|
|||||||
@@ -373,8 +373,6 @@ export function AgentStreamView({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gap = getGapAbove(item, index);
|
|
||||||
|
|
||||||
// Check if this is the end of a turn (before a user message or end of stream when not running)
|
// Check if this is the end of a turn (before a user message or end of stream when not running)
|
||||||
// In inverted list: index-1 is the next item (newer in time)
|
// In inverted list: index-1 is the next item (newer in time)
|
||||||
const nextItem = flatListData[index - 1];
|
const nextItem = flatListData[index - 1];
|
||||||
@@ -386,18 +384,19 @@ export function AgentStreamView({
|
|||||||
const getContent = () => collectTurnContent(index);
|
const getContent = () => collectTurnContent(index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={[stylesheet.streamItemWrapper]}>
|
||||||
style={[
|
|
||||||
stylesheet.streamItemWrapper,
|
|
||||||
gap ? { marginBottom: gap } : null,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{content}
|
{content}
|
||||||
{isEndOfTurn ? <TurnCopyButton getContent={getContent} /> : null}
|
{isEndOfTurn ? <TurnCopyButton getContent={getContent} /> : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[getGapAbove, renderStreamItemContent, flatListData, agent.status, collectTurnContent]
|
[
|
||||||
|
getGapAbove,
|
||||||
|
renderStreamItemContent,
|
||||||
|
flatListData,
|
||||||
|
agent.status,
|
||||||
|
collectTurnContent,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pendingPermissionItems = useMemo(
|
const pendingPermissionItems = useMemo(
|
||||||
@@ -419,7 +418,10 @@ export function AgentStreamView({
|
|||||||
}
|
}
|
||||||
const delta = Math.abs(totalCount - prevCount);
|
const delta = Math.abs(totalCount - prevCount);
|
||||||
streamItemCountRef.current = totalCount;
|
streamItemCountRef.current = totalCount;
|
||||||
if (totalCount < STREAM_ITEM_LOG_MIN_COUNT && delta < STREAM_ITEM_LOG_DELTA_THRESHOLD) {
|
if (
|
||||||
|
totalCount < STREAM_ITEM_LOG_MIN_COUNT &&
|
||||||
|
delta < STREAM_ITEM_LOG_DELTA_THRESHOLD
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let userCount = 0;
|
let userCount = 0;
|
||||||
@@ -452,7 +454,10 @@ export function AgentStreamView({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const metrics = totalCount >= STREAM_ITEM_LOG_MIN_COUNT ? measurePayload(streamItems) : null;
|
const metrics =
|
||||||
|
totalCount >= STREAM_ITEM_LOG_MIN_COUNT
|
||||||
|
? measurePayload(streamItems)
|
||||||
|
: null;
|
||||||
perfLog(AGENT_STREAM_LOG_TAG, {
|
perfLog(AGENT_STREAM_LOG_TAG, {
|
||||||
event: "stream_items",
|
event: "stream_items",
|
||||||
agentId,
|
agentId,
|
||||||
@@ -945,11 +950,10 @@ function PermissionRequestCard({
|
|||||||
const handleResponse = useCallback(
|
const handleResponse = useCallback(
|
||||||
(response: AgentPermissionResponse) => {
|
(response: AgentPermissionResponse) => {
|
||||||
respondToPermission({
|
respondToPermission({
|
||||||
agentId: permission.agentId,
|
agentId: permission.agentId,
|
||||||
requestId: permission.request.id,
|
requestId: permission.request.id,
|
||||||
response,
|
response,
|
||||||
})
|
}).catch((error) => {
|
||||||
.catch((error) => {
|
|
||||||
console.error(
|
console.error(
|
||||||
"[PermissionRequestCard] Failed to respond to permission:",
|
"[PermissionRequestCard] Failed to respond to permission:",
|
||||||
error
|
error
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { View, Text, Pressable, Animated } from "react-native";
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
Pressable,
|
||||||
|
Animated,
|
||||||
|
StyleProp,
|
||||||
|
ViewStyle,
|
||||||
|
} from "react-native";
|
||||||
import {
|
import {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -31,6 +38,7 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
Brain,
|
Brain,
|
||||||
Copy,
|
Copy,
|
||||||
|
TriangleAlertIcon,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -173,14 +181,11 @@ export const UserMessage = memo(function UserMessage({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
userMessageStylesheet.container,
|
userMessageStylesheet.container,
|
||||||
!resolvedDisableOuterSpacing &&
|
isFirstInGroup && { marginTop: theme.spacing[4] },
|
||||||
userMessageStylesheet.containerSpacing,
|
isLastInGroup && { marginBottom: theme.spacing[4] },
|
||||||
!resolvedDisableOuterSpacing &&
|
!isFirstInGroup || !isLastInGroup
|
||||||
isFirstInGroup &&
|
? { marginBottom: theme.spacing[1] }
|
||||||
userMessageStylesheet.containerFirstInGroup,
|
: undefined,
|
||||||
!resolvedDisableOuterSpacing &&
|
|
||||||
isLastInGroup &&
|
|
||||||
userMessageStylesheet.containerLastInGroup,
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -364,7 +369,7 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
|||||||
label: {
|
label: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.medium,
|
fontWeight: theme.fontWeight.normal,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
secondaryLabel: {
|
secondaryLabel: {
|
||||||
@@ -804,8 +809,7 @@ export const ActivityLog = memo(function ActivityLog({
|
|||||||
disabled={!isInteractive}
|
disabled={!isInteractive}
|
||||||
style={[
|
style={[
|
||||||
activityLogStylesheet.pressable,
|
activityLogStylesheet.pressable,
|
||||||
!resolvedDisableOuterSpacing &&
|
!resolvedDisableOuterSpacing && activityLogStylesheet.pressableSpacing,
|
||||||
activityLogStylesheet.pressableSpacing,
|
|
||||||
config.bg,
|
config.bg,
|
||||||
isInteractive && activityLogStylesheet.pressableActive,
|
isInteractive && activityLogStylesheet.pressableActive,
|
||||||
]}
|
]}
|
||||||
@@ -985,8 +989,7 @@ export const TodoListCard = memo(function TodoListCard({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
todoListCardStylesheet.container,
|
todoListCardStylesheet.container,
|
||||||
!resolvedDisableOuterSpacing &&
|
!resolvedDisableOuterSpacing && todoListCardStylesheet.containerSpacing,
|
||||||
todoListCardStylesheet.containerSpacing,
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View style={todoListCardStylesheet.card}>
|
<View style={todoListCardStylesheet.card}>
|
||||||
@@ -1056,6 +1059,7 @@ interface ExpandableBadgeProps {
|
|||||||
secondaryLabel?: string;
|
secondaryLabel?: string;
|
||||||
icon?: ComponentType<{ size?: number; color?: string }>;
|
icon?: ComponentType<{ size?: number; color?: string }>;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
renderDetails?: () => ReactNode;
|
renderDetails?: () => ReactNode;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@@ -1066,6 +1070,7 @@ interface ExpandableBadgeProps {
|
|||||||
|
|
||||||
const ExpandableBadge = memo(function ExpandableBadge({
|
const ExpandableBadge = memo(function ExpandableBadge({
|
||||||
label,
|
label,
|
||||||
|
style,
|
||||||
secondaryLabel,
|
secondaryLabel,
|
||||||
icon,
|
icon,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
@@ -1118,7 +1123,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
const IconComponent = icon;
|
const IconComponent = icon;
|
||||||
const iconColor = isError
|
const iconColor = isError
|
||||||
? theme.colors.destructive
|
? theme.colors.destructive
|
||||||
: theme.colors.foreground;
|
: theme.colors.mutedForeground;
|
||||||
|
|
||||||
let iconNode: ReactNode = null;
|
let iconNode: ReactNode = null;
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -1128,22 +1133,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
} else if (isError) {
|
} else if (isError) {
|
||||||
iconNode = <X size={12} color={iconColor} />;
|
iconNode = <TriangleAlertIcon size={12} color={iconColor} opacity={0.8} />;
|
||||||
} else if (IconComponent) {
|
} else if (IconComponent) {
|
||||||
iconNode = <IconComponent size={12} color={iconColor} />;
|
iconNode = <IconComponent size={12} color={iconColor} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={[expandableBadgeStylesheet.container, style]}>
|
||||||
style={[
|
|
||||||
expandableBadgeStylesheet.container,
|
|
||||||
!resolvedDisableOuterSpacing &&
|
|
||||||
expandableBadgeStylesheet.containerSpacing,
|
|
||||||
!resolvedDisableOuterSpacing &&
|
|
||||||
isLastInSequence &&
|
|
||||||
expandableBadgeStylesheet.containerLastInSequence,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={hasDetails ? onToggle : undefined}
|
onPress={hasDetails ? onToggle : undefined}
|
||||||
disabled={!hasDetails}
|
disabled={!hasDetails}
|
||||||
@@ -1507,12 +1503,13 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
hasDetails && !isMobile
|
hasDetails && !isMobile
|
||||||
? renderDetails
|
? renderDetails
|
||||||
: hasDetails
|
: hasDetails
|
||||||
? () => null
|
? () => null
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
isLoading={status === "executing"}
|
isLoading={status === "executing"}
|
||||||
isError={status === "failed"}
|
isError={status === "failed"}
|
||||||
isLastInSequence={isLastInSequence}
|
isLastInSequence={isLastInSequence}
|
||||||
|
style={isLastInSequence ? undefined : { marginBottom: theme.spacing[1] }}
|
||||||
disableOuterSpacing={disableOuterSpacing}
|
disableOuterSpacing={disableOuterSpacing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ function updateFavicon(status: FaviconStatus, colorScheme: ColorScheme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSystemColorScheme(): ColorScheme {
|
function getSystemColorScheme(): ColorScheme {
|
||||||
if (typeof window === "undefined") return "dark";
|
if (Platform.OS !== "web" || typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
||||||
|
return "dark";
|
||||||
|
}
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ StyleSheet.configure({
|
|||||||
xl: 1200,
|
xl: 1200,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
initialTheme: "dark",
|
adaptiveThemes: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ let child: ChildProcess | null = null;
|
|||||||
let restarting = false;
|
let restarting = false;
|
||||||
|
|
||||||
function spawnServer() {
|
function spawnServer() {
|
||||||
child = fork(serverEntry, {
|
child = fork(serverEntry, process.argv.slice(2), {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
env: process.env,
|
env: process.env,
|
||||||
execArgv: ["--import", "tsx"],
|
execArgv: ["--import", "tsx"],
|
||||||
|
|||||||
@@ -1408,6 +1408,8 @@ export class DaemonClientV2 {
|
|||||||
|
|
||||||
const parsed = WSOutboundMessageSchema.safeParse(parsedJson);
|
const parsed = WSOutboundMessageSchema.safeParse(parsedJson);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
|
const msgType = (parsedJson as { message?: { type?: string } })?.message?.type ?? "unknown";
|
||||||
|
console.warn(`[DaemonClientV2] Message validation failed for "${msgType}":`, parsed.error.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -879,7 +879,10 @@ export class AgentManager {
|
|||||||
event: AgentStreamEvent,
|
event: AgentStreamEvent,
|
||||||
options?: { fromHistory?: boolean }
|
options?: { fromHistory?: boolean }
|
||||||
): void {
|
): void {
|
||||||
agent.updatedAt = new Date();
|
// Only update timestamp for live events, not history replay
|
||||||
|
if (!options?.fromHistory) {
|
||||||
|
agent.updatedAt = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "thread_started":
|
case "thread_started":
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import "dotenv/config";
|
import dotenv from "dotenv";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
|
||||||
import { createPaseoDaemon } from "./bootstrap.js";
|
import { createPaseoDaemon } from "./bootstrap.js";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { resolvePaseoHome } from "./paseo-home.js";
|
import { resolvePaseoHome } from "./paseo-home.js";
|
||||||
|
|||||||
@@ -430,14 +430,10 @@ export const GitDiffRequestSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Highlighted diff token schema
|
// Highlighted diff token schema
|
||||||
|
// Note: style can be a compound class name (e.g., "heading meta") from the syntax highlighter
|
||||||
const HighlightTokenSchema = z.object({
|
const HighlightTokenSchema = z.object({
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
style: z.enum([
|
style: z.string().nullable(),
|
||||||
"keyword", "comment", "string", "number", "literal",
|
|
||||||
"function", "definition", "class", "type", "tag",
|
|
||||||
"attribute", "property", "variable", "operator",
|
|
||||||
"punctuation", "regexp", "escape", "meta", "heading", "link",
|
|
||||||
]).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const DiffLineSchema = z.object({
|
const DiffLineSchema = z.object({
|
||||||
|
|||||||
@@ -468,6 +468,8 @@ function runAgentWithModel(
|
|||||||
"exec",
|
"exec",
|
||||||
"--dangerously-bypass-approvals-and-sandbox",
|
"--dangerously-bypass-approvals-and-sandbox",
|
||||||
"--skip-git-repo-check",
|
"--skip-git-repo-check",
|
||||||
|
"-c",
|
||||||
|
"model_reasoning_effort=\"medium\"",
|
||||||
];
|
];
|
||||||
if (config.model) {
|
if (config.model) {
|
||||||
args.push("--model", config.model);
|
args.push("--model", config.model);
|
||||||
@@ -540,11 +542,28 @@ function makePlannerPrompt(task: Task, context: string, iteration: number): stri
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
You are a PLANNER agent. Your job is to analyze the task tree and organize work to achieve the top-level goal.
|
You are a PLANNER agent. You ORCHESTRATE work - you do NOT do the work yourself.
|
||||||
|
|
||||||
|
Your job is to organize tasks so that WORKER agents can accomplish the top-level acceptance criteria.
|
||||||
|
|
||||||
Current iteration: ${iteration}
|
Current iteration: ${iteration}
|
||||||
|
|
||||||
You have FULL ACCESS to the task CLI and can reorganize the ENTIRE task tree under the scope:
|
## Planning vs Working
|
||||||
|
|
||||||
|
As a planner you MAY:
|
||||||
|
- Look at file structure and test patterns to write good acceptance criteria
|
||||||
|
- Read the task tree to understand what's been done and what needs reorganizing
|
||||||
|
- Check naming conventions to make specific, verifiable criteria
|
||||||
|
|
||||||
|
As a planner you must NOT:
|
||||||
|
- Investigate WHY bugs happen (workers will do this)
|
||||||
|
- Debug or trace through code (workers will do this)
|
||||||
|
- Try to understand root causes (workers will do this)
|
||||||
|
- Fix or implement anything (workers will do this)
|
||||||
|
|
||||||
|
Take the task descriptions AS GIVEN and create well-organized subtasks for workers to investigate and implement.
|
||||||
|
|
||||||
|
## Task CLI Commands
|
||||||
- \`task show ${task.id}\` - view task details with parent context
|
- \`task show ${task.id}\` - view task details with parent context
|
||||||
- \`task list\` - see all tasks
|
- \`task list\` - see all tasks
|
||||||
- \`task tree ${task.id}\` - see full task hierarchy
|
- \`task tree ${task.id}\` - see full task hierarchy
|
||||||
@@ -694,18 +713,17 @@ The ONLY question: Does the acceptance criterion pass when verified? Yes or No.
|
|||||||
|
|
||||||
If an agent left notes explaining why something couldn't be done, IGNORE THE EXPLANATION. Just check: is the criterion met?
|
If an agent left notes explaining why something couldn't be done, IGNORE THE EXPLANATION. Just check: is the criterion met?
|
||||||
|
|
||||||
## Output Format
|
## Output Format - CRITICAL
|
||||||
|
|
||||||
After verification, output EXACTLY one of these XML tags:
|
You MUST output one of these XML tags at the END of your response. This is REQUIRED for the system to parse your verdict:
|
||||||
|
|
||||||
If ALL criteria pass verification:
|
If ALL criteria pass:
|
||||||
<VERDICT>DONE</VERDICT>
|
<VERDICT>DONE</VERDICT>
|
||||||
|
|
||||||
If ANY criterion fails verification:
|
If ANY criterion fails:
|
||||||
<VERDICT>NOT_DONE</VERDICT>
|
<VERDICT>NOT_DONE</VERDICT>
|
||||||
<FAILED_CRITERIA>
|
|
||||||
- criterion that failed: what you checked and what you found
|
If you do not include this exact XML tag, your verdict will not be recorded and the task will retry.
|
||||||
</FAILED_CRITERIA>
|
|
||||||
|
|
||||||
Then add a note to the task with your findings:
|
Then add a note to the task with your findings:
|
||||||
\`task note ${task.id} "Judge verdict: [DONE/NOT_DONE]. Details: ..."\`
|
\`task note ${task.id} "Judge verdict: [DONE/NOT_DONE]. Details: ..."\`
|
||||||
@@ -785,10 +803,42 @@ program
|
|||||||
};
|
};
|
||||||
|
|
||||||
const runTaskLoop = async (): Promise<void> => {
|
const runTaskLoop = async (): Promise<void> => {
|
||||||
|
// First check for in_progress tasks (resuming from crash)
|
||||||
|
const allTasks = await store.list();
|
||||||
|
let candidates = scopeId
|
||||||
|
? [await store.get(scopeId), ...(await store.getDescendants(scopeId))].filter(Boolean) as Task[]
|
||||||
|
: allTasks;
|
||||||
|
|
||||||
|
const inProgress = candidates.filter((t) => t.status === "in_progress");
|
||||||
|
if (inProgress.length > 0) {
|
||||||
|
log(logFile, `Found ${inProgress.length} in_progress task(s) from previous run, resuming...`);
|
||||||
|
for (const t of inProgress) {
|
||||||
|
await store.update(t.id, { status: "open" });
|
||||||
|
log(logFile, `Reset ${t.id} to open`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Initial planner run on scope root (if enabled)
|
||||||
|
if (enablePlanner && scopeId) {
|
||||||
|
const scopeTask = await store.get(scopeId);
|
||||||
|
if (scopeTask) {
|
||||||
|
log(logFile, `[PLANNER] Initial planning on scope root...`);
|
||||||
|
await runPlanner(scopeTask, "initial planning");
|
||||||
|
log(logFile, `[DEBUG] Planner finished, continuing to worker loop`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(logFile, `[DEBUG] Entering worker loop`);
|
||||||
|
|
||||||
|
// Step 2: Worker/Judge loop on ready tasks
|
||||||
while (true) {
|
while (true) {
|
||||||
// Find ready tasks
|
log(logFile, `[DEBUG] Checking for ready tasks...`);
|
||||||
const ready = await store.getReady(scopeId);
|
const ready = await store.getReady(scopeId);
|
||||||
if (ready.length === 0) break;
|
log(logFile, `[DEBUG] Found ${ready.length} ready tasks`);
|
||||||
|
if (ready.length === 0) {
|
||||||
|
log(logFile, `[DEBUG] No ready tasks, exiting loop`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const task = ready[0];
|
const task = ready[0];
|
||||||
const workerModel = (task.assignee === "codex" ? "gpt-5.2" : "sonnet") as ModelName;
|
const workerModel = (task.assignee === "codex" ? "gpt-5.2" : "sonnet") as ModelName;
|
||||||
@@ -796,20 +846,7 @@ program
|
|||||||
log(logFile, `\n=== Starting task: ${task.id} - ${task.title} ===`);
|
log(logFile, `\n=== Starting task: ${task.id} - ${task.title} ===`);
|
||||||
await store.start(task.id);
|
await store.start(task.id);
|
||||||
|
|
||||||
// Step 1: Initial planner run (if enabled)
|
// Worker/Judge loop
|
||||||
if (enablePlanner) {
|
|
||||||
const shouldRestart = await runPlanner(task, "initial planning");
|
|
||||||
if (shouldRestart) {
|
|
||||||
// Reset task to open so we process children first
|
|
||||||
const currentTask = await store.get(task.id);
|
|
||||||
if (currentTask && currentTask.status === "in_progress") {
|
|
||||||
await store.update(task.id, { status: "open" });
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Worker/Judge loop
|
|
||||||
let iteration = 1;
|
let iteration = 1;
|
||||||
let taskDone = false;
|
let taskDone = false;
|
||||||
|
|
||||||
@@ -852,6 +889,7 @@ program
|
|||||||
const scopeTask = scopeId ? await store.get(scopeId) : nextReady[0];
|
const scopeTask = scopeId ? await store.get(scopeId) : nextReady[0];
|
||||||
if (scopeTask) {
|
if (scopeTask) {
|
||||||
await runPlanner(scopeTask, "periodic reassessment");
|
await runPlanner(scopeTask, "periodic reassessment");
|
||||||
|
log(logFile, `[DEBUG] Periodic replan finished, continuing loop`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user