@sgtbeatdown/wolfpack-tasks 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ import type { TaskCore, WakeRequest } from "./task-core";
2
+ import { TaskProtocolError } from "./task-protocol";
3
+ import { createTaskStore } from "./task-store";
4
+ import { createVolatileTaskSession } from "./volatile-task-session";
5
+ import { wolfpackLocalHeaders } from "./wolfpack-local-auth";
6
+
7
+ export interface OwnedTaskCore extends TaskCore {
8
+ /** Fence new work, abort requests, settle active calls, then discard RAM state. */
9
+ close(): Promise<void>;
10
+ }
11
+ export interface ConfiguredTaskCoreOptions {
12
+ readonly sessionName?: string;
13
+ readonly baseUrl?: string;
14
+ readonly fetch?: typeof fetch;
15
+ readonly requestTimeoutMs?: number;
16
+ /** Fresh-lifetime registration after accepted state loss; the extension uses it only for structured relay-loss evidence. */
17
+ readonly rebind?: boolean;
18
+ }
19
+
20
+ /** Normal extension transport. Never probe/fallback to the legacy durable relay. */
21
+ export async function createConfiguredTaskCore(options: ConfiguredTaskCoreOptions = {}, signal?: AbortSignal): Promise<OwnedTaskCore> {
22
+ const sessionName = options.sessionName ?? process.env.WOLFPACK_SESSION_NAME;
23
+ if (!sessionName?.trim()) throw new TaskProtocolError("RELAY_UNAVAILABLE", "WOLFPACK_SESSION_NAME is required for the Wolfpack task adapter");
24
+ const port = process.env.WOLFPACK_PORT ?? "18790";
25
+ if (options.baseUrl === undefined && (!/^[0-9]+$/.test(port) || Number(port) < 1 || Number(port) > 65535)) throw new TypeError("invalid WOLFPACK_PORT");
26
+ const base = new URL(options.baseUrl ?? `http://127.0.0.1:${port}`);
27
+ if (base.username || base.password || base.search || base.hash || base.pathname !== "/") throw new TypeError("relay base URL must be an origin");
28
+ const url = new URL("/api/task-relay/volatile-v1", base).href;
29
+ const store = createTaskStore();
30
+ let session: ReturnType<typeof createVolatileTaskSession> | undefined;
31
+ try {
32
+ const requestFetch = options.fetch ?? fetch;
33
+ const authenticated = Object.assign((input: Parameters<typeof fetch>[0], init?: RequestInit) => requestFetch(input, { ...init, headers: wolfpackLocalHeaders(url, init?.headers) }), { preconnect: requestFetch.preconnect }) as typeof fetch;
34
+ session = createVolatileTaskSession({ url, callerSession: sessionName, store, fetch: authenticated, ...(options.requestTimeoutMs !== undefined && { requestTimeoutMs: options.requestTimeoutMs }) });
35
+ const core = await (options.rebind ? session.rebind(signal) : session.connect(signal));
36
+ const active = new Set<Promise<unknown>>();
37
+ let closed = false, closing: Promise<void> | undefined;
38
+ const owned = { ...core } as OwnedTaskCore;
39
+ const track = <T>(result: Promise<T>): Promise<T> => {
40
+ active.add(result);
41
+ void result.then(() => active.delete(result), () => active.delete(result));
42
+ return result;
43
+ };
44
+ for (const [key, value] of Object.entries(core)) {
45
+ if (typeof value !== "function") continue;
46
+ Object.defineProperty(owned, key, { enumerable: true, value: (...args: unknown[]) => {
47
+ if (closed) throw new TaskProtocolError("RELAY_CLOSED", "task session is closed", { retryable: false });
48
+ // Core methods may call sibling methods through `this` (notably intent ACK).
49
+ // Keep their original receiver while the outer promise owns the full operation.
50
+ const result: unknown = value.apply(core, args);
51
+ if (key === "recordWakeRequest") {
52
+ const request = result as WakeRequest;
53
+ return {
54
+ flush: (signal?: AbortSignal): Promise<void> => {
55
+ if (closed) throw new TaskProtocolError("RELAY_CLOSED", "task session is closed", { retryable: false });
56
+ return track(request.flush(signal));
57
+ },
58
+ // A closed session drops its store; there is nothing left to withdraw.
59
+ discard: () => { if (!closed) request.discard(); },
60
+ };
61
+ }
62
+ return result instanceof Promise ? track(result) : result;
63
+ } });
64
+ }
65
+ owned.close = () => {
66
+ if (closing) return closing;
67
+ closed = true;
68
+ session!.close();
69
+ closing = Promise.allSettled([...active]).then(() => { store.close(); });
70
+ return closing;
71
+ };
72
+ return owned;
73
+ } catch (error) {
74
+ session?.close(); store.close(); throw error;
75
+ }
76
+ }
@@ -0,0 +1,51 @@
1
+ import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+
4
+ import { createAgentTaskRuntime, defaultCoreFactory } from "./agent-task-runtime";
5
+ import type { AgentTaskToolResult, ConfiguredCoreFactory } from "./agent-task-runtime";
6
+ import type { TaskCore } from "./task-core";
7
+
8
+ export {
9
+ ACTIVE_POLL_MS,
10
+ IDLE_POLL_MS,
11
+ WORKER_COORDINATION_FORBIDDEN_CODE,
12
+ WORKER_GATE_DENIAL_CODE,
13
+ createConfiguredCoreLoader,
14
+ createSingleFlightInboxRefresh,
15
+ outboxFailureStatus,
16
+ } from "./agent-task-runtime";
17
+ export type { ConfiguredCoreFactory, ConfiguredCoreLoader } from "./agent-task-runtime";
18
+
19
+ /** Registers endpoint-owned tools using the memory-owned Wolfpack transport by default. */
20
+ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefined = undefined, createCore: ConfiguredCoreFactory = defaultCoreFactory): void {
21
+ // Delegate at call time: hosts and tests may replace these methods after registration.
22
+ const { lifecycle, tools } = createAgentTaskRuntime({
23
+ sendMessage(message, options) { pi.sendMessage(message, options); },
24
+ appendEntry(customType, data) { pi.appendEntry(customType, data); },
25
+ }, core, createCore);
26
+ pi.on("tool_call", lifecycle.tool_call);
27
+ pi.on("session_start", lifecycle.session_start);
28
+ pi.on("agent_end", lifecycle.agent_end);
29
+ pi.on("agent_settled", lifecycle.agent_settled);
30
+ pi.on("turn_end", lifecycle.turn_end);
31
+ pi.on("session_shutdown", lifecycle.session_shutdown);
32
+ for (const tool of tools) {
33
+ pi.registerTool({
34
+ name: tool.name,
35
+ label: tool.label,
36
+ description: tool.description,
37
+ parameters: tool.parameters,
38
+ execute: (toolCallId, params, signal, onUpdate, context): Promise<AgentToolResult<unknown>> => tool.execute(toolCallId, params, signal, onUpdate, context),
39
+ renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
40
+ });
41
+ }
42
+ }
43
+
44
+ export default function piTasks(pi: ExtensionAPI): void {
45
+ registerAgentTaskTools(pi);
46
+ }
47
+
48
+ function text(result: Pick<AgentTaskToolResult, "content"> | { readonly content?: readonly unknown[] }): string {
49
+ const first = result.content?.[0];
50
+ return first && typeof first === "object" && "type" in first && first.type === "text" && "text" in first && typeof first.text === "string" ? first.text : "";
51
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ MAX_RELAY_PAYLOAD_BYTES,
3
+ TASK_PROTOCOL_VERSION,
4
+ TaskProtocolError,
5
+ } from "./task-protocol";
6
+ import type {
7
+ RelayAcceptance,
8
+ RelayConnectInput,
9
+ RelayConnection,
10
+ RelayDeliveryAck,
11
+ RelayEnvelope,
12
+ RelayInboxPage,
13
+ RelayReceiveRequest,
14
+ RelayTargetReference,
15
+ TaskEndpoint,
16
+ TaskRelay,
17
+ } from "./task-protocol";
18
+
19
+ interface MailboxItem {
20
+ readonly cursor: string;
21
+ readonly envelope: RelayEnvelope;
22
+ }
23
+
24
+ /** Deterministic conformance fixture; production relays must provide durable storage. */
25
+ export function createInMemoryTaskRelay(id: string): InMemoryTaskRelay {
26
+ return new InMemoryTaskRelay(id);
27
+ }
28
+
29
+ export class InMemoryTaskRelay implements TaskRelay {
30
+ readonly id: string;
31
+ private readonly endpoints = new Map<string, TaskEndpoint>();
32
+ private readonly mailboxes = new Map<string, MailboxItem[]>();
33
+ private readonly accepted = new Set<string>();
34
+ private readonly acknowledged = new Set<string>();
35
+ private nextDelivery = 0;
36
+ private sendFailure = false;
37
+
38
+ constructor(id: string) {
39
+ this.id = id;
40
+ }
41
+
42
+ async connect(input: RelayConnectInput): Promise<RelayConnection> {
43
+ if (input.protocolVersion !== TASK_PROTOCOL_VERSION) throw new TaskProtocolError("INCOMPATIBLE_PROTOCOL", "task endpoint protocol is incompatible");
44
+ this.assertEndpoint(input.endpoint);
45
+ this.endpoints.set(input.endpoint.id, input.endpoint);
46
+ if (!this.mailboxes.has(input.endpoint.id)) this.mailboxes.set(input.endpoint.id, []);
47
+ return { endpoint: input.endpoint, receiveCursor: input.receiveCursor };
48
+ }
49
+
50
+ async resolve(input: RelayTargetReference): Promise<TaskEndpoint> {
51
+ if (input.relay !== this.id) throw new TaskProtocolError("UNKNOWN_RELAY", "target belongs to another relay");
52
+ const endpoint = this.endpoints.get(input.reference);
53
+ if (!endpoint) throw new TaskProtocolError("UNREGISTERED_TARGET", "target is not registered for this protocol");
54
+ return endpoint;
55
+ }
56
+
57
+ async send(input: RelayEnvelope): Promise<RelayAcceptance> {
58
+ if (this.sendFailure) {
59
+ this.sendFailure = false;
60
+ throw new Error("in-memory relay send failed");
61
+ }
62
+ this.assertEnvelope(input);
63
+ if (this.accepted.has(input.envelopeId)) return { envelopeId: input.envelopeId };
64
+ const mailbox = this.mailboxes.get(input.target.id);
65
+ if (!mailbox) throw new TaskProtocolError("UNREGISTERED_TARGET", "target is not registered for this protocol");
66
+ this.accepted.add(input.envelopeId);
67
+ mailbox.push({ cursor: String(++this.nextDelivery), envelope: input });
68
+ return { envelopeId: input.envelopeId };
69
+ }
70
+
71
+ async receive(input: RelayReceiveRequest): Promise<RelayInboxPage> {
72
+ this.assertEndpoint(input.endpoint);
73
+ const mailbox = this.mailboxes.get(input.endpoint.id);
74
+ if (!mailbox) throw new TaskProtocolError("UNREGISTERED_TARGET", "endpoint is not registered for this protocol");
75
+ const cursor = Number(input.cursor);
76
+ if (!Number.isSafeInteger(cursor) || cursor < 0) throw new TaskProtocolError("INVALID_CURSOR", "relay cursor must be a non-negative integer");
77
+ const start = cursor;
78
+ const pending = mailbox.filter((item) => Number(item.cursor) > start && !this.acknowledged.has(item.envelope.envelopeId));
79
+ const deliveries = pending.slice(0, input.limit);
80
+ return {
81
+ deliveries,
82
+ nextCursor: deliveries.at(-1)?.cursor ?? String(start),
83
+ hasMore: pending.length > deliveries.length,
84
+ };
85
+ }
86
+
87
+ async acknowledgeDelivery(input: RelayDeliveryAck): Promise<void> {
88
+ this.assertEndpoint(input.endpoint);
89
+ const cursor = Number(input.cursor);
90
+ if (!Number.isSafeInteger(cursor) || cursor < 0) throw new TaskProtocolError("INVALID_CURSOR", "relay cursor must be a non-negative integer");
91
+ const item = this.mailboxes.get(input.endpoint.id)?.find(item => item.cursor === input.cursor);
92
+ if (!item) throw new TaskProtocolError("INVALID_CURSOR", "relay delivery is not in this endpoint mailbox");
93
+ this.acknowledged.add(item.envelope.envelopeId);
94
+ }
95
+
96
+ failNextSend(): void {
97
+ this.sendFailure = true;
98
+ }
99
+
100
+ envelopesFor(endpoint: TaskEndpoint): readonly RelayEnvelope[] {
101
+ return (this.mailboxes.get(endpoint.id) ?? []).map((item) => item.envelope);
102
+ }
103
+
104
+ private assertEndpoint(endpoint: TaskEndpoint): void {
105
+ if (endpoint.relay !== this.id || endpoint.id.length === 0) throw new TaskProtocolError("INVALID_ENDPOINT", "endpoint must belong to this relay and have an opaque id");
106
+ }
107
+
108
+ private assertEnvelope(envelope: RelayEnvelope): void {
109
+ if (envelope.protocolVersion !== TASK_PROTOCOL_VERSION) throw new TaskProtocolError("INCOMPATIBLE_PROTOCOL", "envelope protocol is incompatible");
110
+ this.assertEndpoint(envelope.source);
111
+ this.assertEndpoint(envelope.target);
112
+ if (!this.endpoints.has(envelope.source.id)) throw new TaskProtocolError("UNREGISTERED_SOURCE", "source is not registered for this protocol");
113
+ if (new TextEncoder().encode(envelope.payload).byteLength > MAX_RELAY_PAYLOAD_BYTES) throw new TaskProtocolError("PAYLOAD_TOO_LARGE", "relay payload exceeds the protocol limit");
114
+ }
115
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ export { createInMemoryTaskRelay, InMemoryTaskRelay } from "./in-memory-task-relay";
2
+ export { createTaskCore } from "./task-core";
3
+ export { createConfiguredTaskCore } from "./configured-task-core";
4
+ export type { ConfiguredTaskCoreOptions, OwnedTaskCore } from "./configured-task-core";
5
+ export { createTaskStore } from "./task-store";
6
+ export { createVolatileTaskSession, VOLATILE_PROFILE } from "./volatile-task-session";
7
+ export type { VolatileTaskSession, VolatileTaskSessionOptions } from "./volatile-task-session";
8
+ export { WOLFPACK_TASK_RELAY_ID } from "./wolfpack-endpoint-view";
9
+ export { runTaskRelayConformance } from "./task-relay-conformance";
10
+ export {
11
+ MAX_RELAY_PAYLOAD_BYTES,
12
+ TASK_PROTOCOL_VERSION,
13
+ TaskEnvelopeKind,
14
+ TaskProtocolError,
15
+ } from "./task-protocol";
16
+ export type {
17
+ RelayAcceptance,
18
+ RelayConnectInput,
19
+ RelayConnection,
20
+ RelayDelivery,
21
+ RelayDeliveryAck,
22
+ RelayEnvelope,
23
+ RelayInboxPage,
24
+ RelayReceiveRequest,
25
+ RelayTargetReference,
26
+ RelayTransportBinding,
27
+ TaskEndpoint,
28
+ TaskEvent,
29
+ TaskIntent,
30
+ TaskRecord,
31
+ TaskRelay,
32
+ } from "./task-protocol";
33
+ export type { SubmitIntentOutcome, TaskCore, TaskCoreOptions, TaskInboxItem } from "./task-core";
34
+ export type { PendingPresentation, PresentationRecord, TaskStore, TaskStoreOptions } from "./task-store";
35
+ export type { TaskTimers } from "./task-work-scope";