Skip to content
Merged
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
10 changes: 8 additions & 2 deletions src/api/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,15 @@ export async function followRunLogs(
onChunk: (data: string, final: boolean) => void,
signal?: AbortSignal,
follow = true,
projectId?: string,
): Promise<void> {
for await (const chunk of runClient.followRunLogs({ runId, follow, includeMetadata: true }, { signal }))
onChunk(chunk.data, chunk.isFinal);
// Always request the complete persisted log before following live output.
// In particular, do not use a tail/start offset when opening a run detail
// page: the server treats an omitted tail as the full log history.
for await (const chunk of runClient.followRunLogs(
{ projectId, runId, follow, includeMetadata: true, startOffset: 0n, tailLines: 0, tailSet: false },
{ signal },
)) onChunk(chunk.data, chunk.isFinal);
}

export type ProjectRunDebugTarget = { runId: string; sandboxId: string };
Expand Down
38 changes: 33 additions & 5 deletions src/lib/components/sandbox-log-runs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
type RunEvent,
type RunSummary,
} from '../../gen/agentcompose/v2/agentcompose_pb.js';
import type { SandboxHistoryCell } from '../../api/sessions';
import { compactIdentifier } from '../../model/identifiers';
import { timestampToISOString } from '../../model/timestamps';
import { formatBeijingTime } from '../../time';
Expand All @@ -20,7 +21,14 @@
runs,
events,
activeStream,
}: { sandboxId: string; runs: RunSummary[]; events: RunEvent[]; activeStream?: AgentStreamState } = $props();
legacyCells = [],
}: {
sandboxId: string;
runs: RunSummary[];
events: RunEvent[];
activeStream?: AgentStreamState;
legacyCells?: SandboxHistoryCell[];
} = $props();

let logs = $state<Record<string, string>>({});
let errors = $state<Record<string, string>>({});
Expand All @@ -30,7 +38,18 @@
const requested = new SvelteSet<string>();

const chronologicalRuns = $derived([...runs].reverse());
const content = $derived(chronologicalRuns.map(runSection).join('\n\n'));
const logSections = $derived(
[
...chronologicalRuns.map((run) => ({
createdAt: timestampToISOString(run.startedAt || run.createdAt),
content: runSection(run),
})),
...legacyCells
.filter((cell) => cell.source.trim() || cell.output.trim())
.map((cell) => ({ createdAt: cell.createdAt, content: legacyCellSection(cell) })),
].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt)),
);
const content = $derived(logSections.map((section) => section.content).join('\n\n'));
const visibleContent = $derived(
query.trim()
? content
Expand All @@ -53,11 +72,11 @@
async function loadRuns(items: RunSummary[]): Promise<void> {
const completed = items.filter((run) => run.status !== RunStatus.RUNNING);
const running = items.filter((run) => run.status === RunStatus.RUNNING);
for (const run of completed) await load(run.runId, false);
for (const run of running) void load(run.runId, true);
for (const run of completed) await load(run.runId, false, run.projectId);
for (const run of running) void load(run.runId, true, run.projectId);
}

async function load(runId: string, follow: boolean): Promise<void> {
async function load(runId: string, follow: boolean, projectId: string): Promise<void> {
const controller = new AbortController();
controllers.set(runId, controller);
logs = { ...logs, [runId]: '' };
Expand All @@ -67,6 +86,7 @@
(chunk) => (logs = { ...logs, [runId]: `${logs[runId] ?? ''}${chunk}` }),
controller.signal,
follow,
projectId,
);
} catch (cause) {
if (!controller.signal.aborted)
Expand Down Expand Up @@ -94,6 +114,14 @@
return `${heading}\n${t(loaded[run.runId] ? '没有日志输出' : '正在加载日志…')}`;
}

function legacyCellSection(cell: SandboxHistoryCell): string {
const at = formatBeijingTime(cell.createdAt);
const shortCellId = compactIdentifier(cell.id);
const heading = `──── ${at} · ${t('执行历史')} · ${shortCellId} · ${cell.success ? t('成功') : t('失败')} ────`;
const body = [cell.source.trim(), cell.output.trim(), cell.stopReason.trim()].filter(Boolean).join('\n');
return `${heading}\n${body}`;
}

function eventSection(runId: string): string {
const persisted = events
.filter(
Expand Down
32 changes: 28 additions & 4 deletions src/lib/components/sandbox-workbench.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
stopSandboxContext,
watchSandbox,
type SandboxContextDetail,
type SandboxHistoryCell,
type SandboxHistoryEvent,
type SandboxRunTarget,
} from '../../api/sessions';
Expand Down Expand Up @@ -60,6 +61,7 @@
let conversationRuns = $state<RunSummary[]>([]);
let events = $state<RunEvent[]>([]);
let historyEvents = $state<SandboxHistoryEvent[]>([]);
let historyCells = $state<SandboxHistoryCell[]>([]);
let turns = $state<ConversationTurn[]>([]);
let target = $state<SandboxRunTarget | undefined>();
let selectedTargetKey = $state('');
Expand All @@ -86,8 +88,20 @@

const activeStream = $derived(runStreams.forSandbox(sandboxId));
const hasConversation = $derived(conversationRuns.length > 0 || Boolean(activeStream?.prompt));
const legacyHistoryEntries = $derived(
historyCells
.filter((cell) => (!runs.length || !cell.runId) && (cell.source.trim() || cell.output.trim()))
.map((cell) => ({
id: `history-cell-${cell.id}`,
createdAt: cell.createdAt,
type: 'history.cell',
message: [cell.source.trim(), cell.output.trim()].filter(Boolean).join('\n'),
})),
);
const sortedLogEntries = $derived(
[...contextLogEntries, ...liveEntries, ...historyEvents].sort((left, right) => sortTime(left) - sortTime(right)),
[...contextLogEntries, ...liveEntries, ...historyEvents, ...legacyHistoryEntries].sort(
(left, right) => sortTime(left) - sortTime(right),
),
);
const combinedLog = $derived(
sortedLogEntries
Expand Down Expand Up @@ -195,11 +209,13 @@
conversationRuns = nextConversationRuns;
events = nextEvents;
historyEvents = nextHistoryEvents;
historyCells = cells;
turns = nextTurns;
target = nextTarget;
selectedTargetKey = targetKey(nextTarget ?? firstTarget(nextRuns));
const conversationAvailable = nextConversationRuns.length > 0;
const logsAvailable = nextRuns.length > 0 || nextHistoryEvents.length > 0 || contextLogEntries.length > 0;
const logsAvailable =
nextRuns.length > 0 || nextHistoryEvents.length > 0 || cells.length > 0 || contextLogEntries.length > 0;
tab =
initialTab === 'records'
? 'records'
Expand All @@ -220,6 +236,7 @@
conversationRuns = [];
events = [];
historyEvents = [];
historyCells = [];
turns = [];
} finally {
if (loadedSandboxId === targetSandboxId) loading = false;
Expand Down Expand Up @@ -303,6 +320,7 @@
runs = nextRuns;
conversationRuns = nextConversationRuns;
events = nextEvents;
historyCells = cells;
turns = nextTurns;
sandbox = nextSandbox;
if (activeStream?.prompt) tab = 'conversation';
Expand Down Expand Up @@ -518,7 +536,7 @@
<Tabs.Root bind:value={tab} class="flex min-h-0 flex-1 flex-col overflow-hidden">
<Tabs.List data-tab-scroll class="shrink-0 justify-start">
{#if hasConversation || runnable}<Tabs.Trigger value="conversation">{t('对话')}</Tabs.Trigger>{/if}
{#if runs.length || contextLogEntries.length || liveEntries.length || historyEvents.length}<Tabs.Trigger
{#if runs.length || contextLogEntries.length || liveEntries.length || historyEvents.length || historyCells.length}<Tabs.Trigger
value="logs">{t('运行日志')}</Tabs.Trigger
>{/if}
<Tabs.Trigger value="records">{t('智能体记录')}</Tabs.Trigger>
Expand All @@ -529,7 +547,13 @@
<SandboxConversation runs={conversationRuns} {events} {turns} {activeStream} />
</Tabs.Content>
<Tabs.Content value="logs" class="mt-3 min-h-0 flex-1 overflow-hidden">
{#if runs.length}<SandboxLogRuns {sandboxId} {runs} {events} {activeStream} />
{#if runs.length}<SandboxLogRuns
{sandboxId}
{runs}
{events}
{activeStream}
legacyCells={historyCells.filter((cell) => !cell.runId && !cell.id.endsWith('-legacy-log'))}
/>
{:else}<RunLogViewer
query={contextLogQuery}
content={visibleCombinedLog}
Expand Down
2 changes: 2 additions & 0 deletions src/routes/RunDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@
logs += chunk;
},
controller.signal,
true,
nextDetail.summary?.projectId,
).catch((cause) => {
if (version === loadVersion && !controller?.signal.aborted)
error = t('日志订阅断开:{error}', { error: errorMessage(cause) });
Expand Down
Loading