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
160 changes: 159 additions & 1 deletion apps/chatgpt/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import {
runTask,
checkTask,
listSessions,
listTasks,
getSession,
agentBlurb,
agentDescriptor,
searchMemory,
getMemory,
listCollections,
} from "@clawconnect/core";
import type { AgentEntry, AgentRegistry, CheckMode } from "@clawconnect/core";
import type { AgentEntry, AgentRegistry, CheckMode, TaskSummary } from "@clawconnect/core";

// Widget UI is temporarily disabled to keep the surface focused on
// run_task / check_task. Re-enable by restoring the widget imports and
Expand Down Expand Up @@ -144,6 +146,14 @@ function resolveScope(url: URL): Scope {
return { allowedIds: allowed, defaultId, serverName };
}

/** Non-terminal task statuses — tasks that still need attention. */
const ACTIVE_STATUSES: ReadonlySet<TaskSummary["status"]> = new Set([
"queued",
"running",
"blocked",
"needs-human",
]);

const AGENTS_BY_ID = new Map<string, AgentEntry>(registry.agents.map((a) => [a.id, a]));

function blurbsFor(ids: string[]): string {
Expand Down Expand Up @@ -296,6 +306,64 @@ Pass the jobId returned by run_task. Available agents: ${list}.`,
inputSchema: { type: "object", properties: {} },
annotations: { title: "List Collections", readOnlyHint: true, idempotentHint: true, openWorldHint: false },
},
{
name: "list_tasks",
description: `List manager-friendly task summaries across agents. This is task-level coordination (what needs attention), not low-level session debugging.`,
inputSchema: {
type: "object",
properties: {
view: {
type: "string",
enum: ["active", "all"],
description: 'Optional preset. "active" returns non-terminal tasks (queued, running, blocked, needs-human) that still need attention.',
},
},
},
annotations: { title: "List Tasks", readOnlyHint: true, idempotentHint: true, openWorldHint: false },
},
{
name: "get_task",
description: `Inspect a task by taskId/jobId with a detail preset controlling which fields are returned.`,
inputSchema: {
type: "object",
properties: {
taskId: { type: "string", description: "Task identifier (same as jobId in v1)" },
detail: {
type: "string",
enum: ["core", "summary", "updates", "artifacts", "diagnostics", "full", "fullWithDiagnostics"],
description:
'Detail preset. Omit for summary. core=ids+status only; summary=+summary; updates=+logs; artifacts=+artifacts; diagnostics=+error info; full=core+summary+updates+artifacts; fullWithDiagnostics=full+diagnostics',
},
mode: {
type: "string",
enum: ["poll", "wait"],
description: 'Uses check semantics: "wait" blocks up to timeout; "poll" returns on updates',
},
},
required: ["taskId"],
},
annotations: { title: "Get Task", readOnlyHint: true, idempotentHint: true, openWorldHint: false },
},
{
name: "get_session",
description: `Inspect one session for debugging ("what exactly happened?"). Use mode="snapshot" for current state, "events" for bounded event retrieval, or "tail" for cursor-based tailing.`,
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string", description: "Session key to inspect" },
mode: {
type: "string",
enum: ["snapshot", "events", "tail"],
description: "Inspection mode: snapshot (default), events, or tail",
},
limit: { type: "number", description: "Max events to return for events/tail modes (1–200)" },
after: { type: "number", description: "Zero-based event cursor; for tail mode use returned nextAfter" },
agent: { ...agentProp, description: `${agentProp.description} Usually inferred from sessionId.` },
},
required: ["sessionId"],
},
annotations: { title: "Get Session", readOnlyHint: true, idempotentHint: true, openWorldHint: false },
},
];
}

Expand Down Expand Up @@ -515,6 +583,96 @@ const server = createServer(async (req, res) => {
content: [{ type: "text", text: JSON.stringify({ collections }) }],
structuredContent: { collections },
});
} else if (name === "list_tasks") {
const tasks = listTasks(pool);
const scoped = tasks.filter((t) => !t.agent || scope.allowedIds.includes(t.agent));
const view = typeof args.view === "string" ? args.view : undefined;
const filtered = view === "active" ? scoped.filter((t) => ACTIVE_STATUSES.has(t.status)) : scoped;
respond({
content: [{ type: "text", text: JSON.stringify({ tasks: filtered }) }],
structuredContent: { tasks: filtered },
});
} else if (name === "get_task") {
const taskId = typeof args.taskId === "string" ? args.taskId : "";
const detail = typeof args.detail === "string" ? args.detail : undefined;
const mode = (typeof args.mode === "string" ? args.mode : undefined) as CheckMode | undefined;
const result = await checkTask(pool, { jobId: taskId, mode: mode ?? "wait" });

if (!result.found) {
respond({
content: [{ type: "text", text: "Task not found. The server may have restarted." }],
structuredContent: { taskId, status: "error", error: "Task not found." },
isError: true,
});
} else if (result.snapshot.agent && !scope.allowedIds.includes(result.snapshot.agent)) {
respond({
content: [{ type: "text", text: "Task not found." }],
structuredContent: { taskId, status: "error", error: "Task not found." },
isError: true,
});
} else {
const s = result.snapshot;
const d = detail ?? "summary";
const has = (field: string) => d === field || d === "full" || d === "fullWithDiagnostics";
const payload: Record<string, unknown> = {
taskId: s.jobId,
jobId: s.jobId,
sessionKey: s.sessionKey,
agent: s.agent,
status: s.status,
startedAt: s.startedAt,
lastEventAt: s.lastEventAt,
};
if (d === "summary" || has("summary")) {
payload.summary = s.summary;
}
if (has("updates")) {
payload.updates = s.logs;
}
if (has("artifacts")) {
payload.artifacts = s.artifacts;
}
if (d === "diagnostics" || d === "fullWithDiagnostics") {
payload.diagnostics = { error: s.error, errorInfo: s.errorInfo, continuationState: s.continuationState };
}
respond({
content: [{ type: "text", text: JSON.stringify(payload) }],
structuredContent: payload,
...(result.isError ? { isError: true } : {}),
});
}
} else if (name === "get_session") {
const sessionId = typeof args.sessionId === "string" ? args.sessionId : "";
const sessionMode = typeof args.mode === "string" ? args.mode : undefined;
const limit = args.limit !== undefined ? Number(args.limit) : undefined;
const after = args.after !== undefined ? Number(args.after) : undefined;
const sessionAgent = typeof args.agent === "string" && args.agent ? args.agent : undefined;

if (sessionAgent && !scope.allowedIds.includes(sessionAgent)) {
respond({
content: [{ type: "text", text: `Agent "${sessionAgent}" is not available on this connection.` }],
isError: true,
});
} else {
const result = getSession(pool, { sessionId, mode: sessionMode as any, limit, after, agent: sessionAgent });
if (!result.found) {
respond({
content: [{ type: "text", text: "Session not found." }],
structuredContent: { sessionId, found: false },
isError: true,
});
} else if (result.agent && !scope.allowedIds.includes(result.agent)) {
respond({
content: [{ type: "text", text: "Session not found." }],
isError: true,
});
} else {
respond({
content: [{ type: "text", text: JSON.stringify(result) }],
structuredContent: result,
});
}
}
} else {
respondError(-32601, `Unknown tool: ${name}`);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export { searchMemory, getMemory, listCollections, DEFAULT_QMD_URL } from "./mem
export type { MemorySearchHit, MemorySearchResult, GetMemoryResult, SearchMemoryOpts, CollectionListing } from "./memory.ts";
export { classifyError } from "./errors.ts";
export { emptyArtifacts, processEvent, extractPatternsFromSummary, deriveNextStep } from "./artifacts.ts";
export { runTask, checkTask, listSessions } from "./tools.ts";
export { runTask, checkTask, listSessions, listTasks, getSession } from "./tools.ts";
export type {
Artifacts,
CheckMode,
Expand All @@ -23,5 +23,9 @@ export type {
JobStatus,
LogEntry,
RunTaskResult,
SessionInspectMode,
SessionInspectResult,
TaskInput,
TaskSummary,
TaskStatus,
} from "./types.ts";
79 changes: 79 additions & 0 deletions packages/core/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type {
CheckTaskResult,
ContinuationState,
RunTaskResult,
SessionInspectMode,
SessionInspectResult,
TaskSummary,
TaskInput,
} from "./types.ts";

Expand All @@ -18,12 +21,52 @@ export function runTask(pool: GatewayPool, input: TaskInput): RunTaskResult {
pool.rememberJob(job.jobId, entry.agent.id);
return {
jobId: job.jobId,
taskId: job.jobId,
sessionKey: job.sessionKey,
status: "running",
agent: entry.agent.id,
};
}

function mapTaskStatus(status: string): TaskSummary["status"] {
if (status === "running") return "running";
if (status === "completed" || status === "completed_no_summary") return "done";
if (status === "needs-human") return "needs-human";
if (status === "blocked") return "blocked";
if (status === "queued") return "queued";
return "failed";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function deriveTaskStatus(job: { status: string; error?: string; artifacts: { needsHumanDecision: boolean } }): TaskSummary["status"] {
if (job.status === "running") return "running";
if (job.status === "completed" || job.status === "completed_no_summary") return "done";
if (job.artifacts.needsHumanDecision) return "needs-human";
if (job.error?.includes("session busy")) return "blocked";
return mapTaskStatus(job.status);
}

export function listTasks(pool: GatewayPool): TaskSummary[] {
const items: TaskSummary[] = [];
for (const entry of pool.allEntries()) {
for (const session of entry.sessions.listSessions()) {
const job = entry.sessions.getLatestJobForSession(session.sessionKey);
if (!job) continue;
items.push({
taskId: job.jobId,
jobId: job.jobId,
sessionKey: job.sessionKey,
agent: entry.agent.id,
status: deriveTaskStatus(job),
startedAt: job.startedAt,
lastEventAt: job.lastEventAt,
summary: job.summary,
error: job.error,
});
}
}
return items;
}

function notFound(): CheckTaskResult {
return { found: false };
}
Expand Down Expand Up @@ -71,3 +114,39 @@ export function listSessions(pool: GatewayPool): ContinuationState[] {
return all;
}

export function getSession(
pool: GatewayPool,
opts: { sessionId: string; mode?: SessionInspectMode; limit?: number; after?: number; agent?: string },
): SessionInspectResult {
let entry = opts.agent ? pool.forAgent(opts.agent) : pool.forSession(opts.sessionId);
if (!entry) {
for (const candidate of pool.allEntries()) {
if (candidate.sessions.getSessionState(opts.sessionId)) {
entry = candidate;
break;
}
}
}
if (!entry) return { found: false };
const job = entry.sessions.getLatestJobForSession(opts.sessionId);
if (!job) return { found: false };

const mode = opts.mode ?? "snapshot";
const limit = Math.max(1, Math.min(200, opts.limit ?? 50));
const after = Math.max(0, opts.after ?? 0);
const events = job.logs.slice(after, after + limit);

return {
found: true,
sessionKey: job.sessionKey,
agent: entry.agent.id,
jobId: job.jobId,
status: job.status,
startedAt: job.startedAt,
lastEventAt: job.lastEventAt,
summary: job.summary,
error: job.error,
...(mode === "snapshot" ? {} : { events }),
...(mode === "tail" ? { nextAfter: after + events.length } : {}),
};
}
32 changes: 32 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type ErrorInfo = {
export type LogEntry = { ts: number; type: string; text: string };

export type JobStatus = "running" | "completed" | "completed_no_summary" | "error";
export type TaskStatus = "queued" | "running" | "blocked" | "needs-human" | "done" | "failed";

export type Job = {
jobId: string;
Expand Down Expand Up @@ -111,12 +112,43 @@ export type CheckMode = "poll" | "wait";

export type RunTaskResult = {
jobId: string;
taskId?: string;
sessionKey: string;
status: "running";
/** ClawConnect agent alias the task was dispatched to. */
agent?: string;
};

export type TaskSummary = {
taskId: string;
jobId: string;
sessionKey: string;
agent?: string;
status: TaskStatus;
startedAt: number;
lastEventAt: number;
summary?: string;
error?: string;
};

export type SessionInspectMode = "snapshot" | "events" | "tail";

export type SessionInspectResult =
| { found: false }
| {
found: true;
sessionKey: string;
agent?: string;
jobId: string;
status: JobStatus;
startedAt: number;
lastEventAt: number;
summary?: string;
error?: string;
events?: LogEntry[];
nextAfter?: number;
};

export type CheckTaskOpts = {
jobId?: string;
sessionKey?: string;
Expand Down
Loading