mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(daemon): preserve Orbit no-artifact explanation (#1576)
This commit is contained in:
parent
38a5ab69e6
commit
bd0ac2c703
4 changed files with 130 additions and 1 deletions
39
apps/daemon/src/orbit-agent-summary.ts
Normal file
39
apps/daemon/src/orbit-agent-summary.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
const NO_LIVE_ARTIFACT_SUMMARY =
|
||||
'Agent succeeded but did not register a live artifact for this Orbit run.';
|
||||
|
||||
const MAX_FINAL_EXPLANATION_CHARS = 2_000;
|
||||
|
||||
interface RunEventRecord {
|
||||
event?: unknown;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
function asObject(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function textDeltaFromEvent(record: RunEventRecord): string | null {
|
||||
if (record.event !== 'agent') return null;
|
||||
const data = asObject(record.data);
|
||||
if (!data || data.type !== 'text_delta') return null;
|
||||
return typeof data.delta === 'string' ? data.delta : null;
|
||||
}
|
||||
|
||||
export function extractOrbitAgentFinalExplanation(events: readonly RunEventRecord[]): string | null {
|
||||
const text = events
|
||||
.map(textDeltaFromEvent)
|
||||
.filter((delta): delta is string => delta !== null)
|
||||
.join('')
|
||||
.trim();
|
||||
if (!text) return null;
|
||||
if (text.length <= MAX_FINAL_EXPLANATION_CHARS) return text;
|
||||
return `${text.slice(0, MAX_FINAL_EXPLANATION_CHARS).trimEnd()}...`;
|
||||
}
|
||||
|
||||
export function buildOrbitNoLiveArtifactSummary(events: readonly RunEventRecord[]): string {
|
||||
const explanation = extractOrbitAgentFinalExplanation(events);
|
||||
return explanation
|
||||
? `${NO_LIVE_ARTIFACT_SUMMARY}\n\n${explanation}`
|
||||
: NO_LIVE_ARTIFACT_SUMMARY;
|
||||
}
|
||||
|
|
@ -155,6 +155,7 @@ import {
|
|||
} from './mcp-tokens.js';
|
||||
import { agentCliEnvForAgent, readAppConfig, writeAppConfig } from './app-config.js';
|
||||
import { OrbitService, formatLocalProjectTimestamp, renderOrbitTemplateSystemPrompt } from './orbit.js';
|
||||
import { buildOrbitNoLiveArtifactSummary } from './orbit-agent-summary.js';
|
||||
import {
|
||||
RoutineService,
|
||||
validateSchedule as validateRoutineSchedule,
|
||||
|
|
@ -4657,7 +4658,9 @@ export async function startServer({
|
|||
...(artifact?.id ? { artifactId: artifact.id, artifactProjectId: projectId } : {}),
|
||||
summary: artifact?.id
|
||||
? `Agent ${finalStatus.status} and registered live artifact ${artifact.title}.`
|
||||
: `Agent ${finalStatus.status} but did not register a live artifact for this Orbit run.`,
|
||||
: finalStatus.status === 'succeeded'
|
||||
? buildOrbitNoLiveArtifactSummary(run.events)
|
||||
: `Agent ${finalStatus.status} but did not register a live artifact for this Orbit run.`,
|
||||
};
|
||||
})();
|
||||
|
||||
|
|
|
|||
50
apps/daemon/tests/orbit-agent-summary.test.ts
Normal file
50
apps/daemon/tests/orbit-agent-summary.test.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildOrbitNoLiveArtifactSummary,
|
||||
extractOrbitAgentFinalExplanation,
|
||||
} from '../src/orbit-agent-summary.js';
|
||||
|
||||
describe('Orbit agent summary helpers', () => {
|
||||
it('preserves the agent final explanation for no-live-artifact Orbit runs', () => {
|
||||
const summary = buildOrbitNoLiveArtifactSummary([
|
||||
{ event: 'agent', data: { type: 'text_delta', delta: 'Data loading failed, ' } },
|
||||
{ event: 'agent', data: { type: 'text_delta', delta: 'so I did not create a daily digest artifact.' } },
|
||||
]);
|
||||
|
||||
expect(summary).toContain(
|
||||
'Agent succeeded but did not register a live artifact for this Orbit run.',
|
||||
);
|
||||
expect(summary).toContain(
|
||||
'Data loading failed, so I did not create a daily digest artifact.',
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts only user-visible text deltas from run events', () => {
|
||||
expect(
|
||||
extractOrbitAgentFinalExplanation([
|
||||
{ event: 'stdout', data: { chunk: 'raw tool output' } },
|
||||
{ event: 'stderr', data: { chunk: 'OPENAI_API_KEY=sk-raw-secret' } },
|
||||
{ event: 'tool_result', data: { output: 'token=raw-tool-secret' } },
|
||||
{ event: 'agent', data: { type: 'thinking_delta', delta: 'private reasoning' } },
|
||||
{ event: 'agent', data: { type: 'tool_use', name: 'Read' } },
|
||||
{ event: 'agent', data: { type: 'text_delta', delta: 'GitHub auth failed.' } },
|
||||
]),
|
||||
).toBe('GitHub auth failed.');
|
||||
});
|
||||
|
||||
it('falls back to the implementation-level no-artifact marker without final text', () => {
|
||||
expect(buildOrbitNoLiveArtifactSummary([])).toBe(
|
||||
'Agent succeeded but did not register a live artifact for this Orbit run.',
|
||||
);
|
||||
});
|
||||
|
||||
it('bounds long final explanations before storing them in the Orbit receipt', () => {
|
||||
const explanation = extractOrbitAgentFinalExplanation([
|
||||
{ event: 'agent', data: { type: 'text_delta', delta: 'x'.repeat(2_100) } },
|
||||
]);
|
||||
|
||||
expect(explanation).toHaveLength(2_003);
|
||||
expect(explanation?.endsWith('...')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -277,6 +277,43 @@ describe('OrbitService', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('persists failed Orbit agent summaries in the last-run receipt markdown', async () => {
|
||||
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-'));
|
||||
try {
|
||||
const service = new OrbitService(dataDir);
|
||||
service.setRunHandler(async () => ({
|
||||
projectId: 'project-1',
|
||||
agentRunId: 'agent-1',
|
||||
completion: Promise.resolve({
|
||||
agentRunId: 'agent-1',
|
||||
status: 'failed',
|
||||
summary:
|
||||
'Agent succeeded but did not register a live artifact for this Orbit run.\n\nGitHub auth failed, so I did not create a daily digest artifact.',
|
||||
}),
|
||||
}));
|
||||
|
||||
await service.start('manual');
|
||||
let status = await service.status();
|
||||
for (let attempt = 0; attempt < 10 && (status.running || !status.lastRun); attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
status = await service.status();
|
||||
}
|
||||
|
||||
expect(status.lastRun).not.toBeNull();
|
||||
expect(status.running).toBe(false);
|
||||
expect(status.lastRun?.connectorsSucceeded).toBe(0);
|
||||
expect(status.lastRun?.connectorsFailed).toBe(1);
|
||||
expect(status.lastRun?.markdown).toContain(
|
||||
'Agent succeeded but did not register a live artifact for this Orbit run.',
|
||||
);
|
||||
expect(status.lastRun?.markdown).toContain(
|
||||
'GitHub auth failed, so I did not create a daily digest artifact.',
|
||||
);
|
||||
} finally {
|
||||
await rm(dataDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('tracks the most recent run per template alongside the global last run', async () => {
|
||||
vi.useFakeTimers();
|
||||
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue