@hilbras/omninode 2.0.0-alpha.1 → 2.0.0-alpha.10

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.
package/dist/index.js CHANGED
@@ -1,3 +1,12 @@
1
+ // src/types/task.ts
2
+ var TERMINAL_TASK_STATUSES = [
3
+ "completed",
4
+ "failed",
5
+ "timed_out",
6
+ "cancelled",
7
+ "partially_completed"
8
+ ];
9
+
1
10
  // src/errors/index.ts
2
11
  var OmniNodeError = class extends Error {
3
12
  code;
@@ -116,6 +125,7 @@ var providerConfigSchema = z.object({
116
125
  type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
117
126
  base_url: z.string().url(),
118
127
  api_key_env_var: z.string().min(1).optional(),
128
+ timeout_ms: z.number().int().positive().optional(),
119
129
  headers: z.record(z.string()).optional(),
120
130
  enabled: z.boolean().optional(),
121
131
  metadata: z.record(z.unknown()).optional()
@@ -131,6 +141,11 @@ var agentConfigSchema = z.object({
131
141
  input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
132
142
  timeout_ms: z.number().int().positive().optional(),
133
143
  inherit_env: z.boolean().optional(),
144
+ env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
145
+ env_allowlist: z.array(z.string()).optional(),
146
+ env_denylist: z.array(z.string()).optional(),
147
+ max_output_bytes: z.number().int().positive().optional(),
148
+ allow_external_cwd: z.boolean().optional(),
134
149
  metadata: z.record(z.unknown()).optional()
135
150
  }).strict();
136
151
  var roleConfigSchema = z.object({
@@ -175,7 +190,9 @@ var projectConfigSchema = z.object({
175
190
  memory: z.object({
176
191
  provider: z.string().min(1).default("local"),
177
192
  base_url: z.string().url().optional(),
178
- api_key_env_var: z.string().min(1).optional()
193
+ api_key_env_var: z.string().min(1).optional(),
194
+ /** When true, memory failures fail tasks instead of being tolerated (§12). */
195
+ required: z.boolean().optional()
179
196
  }).strict().optional()
180
197
  }).strict();
181
198
  var appConfigSchema = z.object({
@@ -257,7 +274,8 @@ function toAppConfig(yaml) {
257
274
  memory: {
258
275
  provider: yaml.project.memory.provider,
259
276
  baseUrl: yaml.project.memory.base_url,
260
- apiKeyEnvVar: yaml.project.memory.api_key_env_var
277
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var,
278
+ required: yaml.project.memory.required
261
279
  }
262
280
  } : {}
263
281
  },
@@ -288,6 +306,7 @@ function toProviderConfig(raw) {
288
306
  type: raw.type,
289
307
  baseUrl: raw.base_url,
290
308
  apiKeyEnvVar: raw.api_key_env_var,
309
+ timeoutMs: raw.timeout_ms,
291
310
  headers: raw.headers,
292
311
  enabled: raw.enabled,
293
312
  metadata: raw.metadata
@@ -305,6 +324,11 @@ function toAgentConfig(raw) {
305
324
  inputMode: raw.input_mode,
306
325
  timeoutMs: raw.timeout_ms,
307
326
  inheritEnv: raw.inherit_env,
327
+ envPolicy: raw.env_policy,
328
+ envAllowlist: raw.env_allowlist,
329
+ envDenylist: raw.env_denylist,
330
+ maxOutputBytes: raw.max_output_bytes,
331
+ allowExternalCwd: raw.allow_external_cwd,
308
332
  metadata: raw.metadata
309
333
  };
310
334
  }
@@ -375,57 +399,200 @@ function authHeaders(config, env = process.env) {
375
399
  }
376
400
 
377
401
  // src/providers/http.ts
402
+ var HttpResponseError = class extends Error {
403
+ constructor(message, status, cause) {
404
+ super(message, { cause });
405
+ this.status = status;
406
+ this.name = "HttpResponseError";
407
+ }
408
+ status;
409
+ };
378
410
  var DEFAULT_TIMEOUT_MS = 3e4;
379
411
  async function requestJson(url, options = {}) {
380
412
  const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
381
- let response;
382
- try {
383
- response = await fetch(url, {
384
- method,
385
- headers: {
386
- accept: "application/json",
387
- ...body !== void 0 ? { "content-type": "application/json" } : {},
388
- ...headers
389
- },
390
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
391
- signal: AbortSignal.timeout(timeoutMs)
392
- });
393
- } catch (error) {
394
- const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
395
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
396
- cause: error
397
- });
398
- }
413
+ const response = await fetch(url, {
414
+ method,
415
+ headers: {
416
+ accept: "application/json",
417
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
418
+ ...headers
419
+ },
420
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
421
+ signal: AbortSignal.timeout(timeoutMs)
422
+ });
399
423
  let parsed;
400
424
  try {
401
425
  parsed = await response.json();
402
426
  } catch (error) {
403
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Response from ${url} was not valid JSON.`, {
404
- cause: error,
405
- details: { status: response.status }
406
- });
427
+ throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
407
428
  }
408
429
  return { status: response.status, body: parsed };
409
430
  }
410
431
 
432
+ // src/providers/errors.ts
433
+ function kindToCode(kind) {
434
+ switch (kind) {
435
+ case "AUTHENTICATION_ERROR":
436
+ return "PROVIDER_AUTH_FAILED";
437
+ case "MODEL_NOT_FOUND":
438
+ return "MODEL_NOT_FOUND";
439
+ default:
440
+ return "PROVIDER_UNAVAILABLE";
441
+ }
442
+ }
443
+ function isRetryable(kind) {
444
+ return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
445
+ }
446
+ function kindFromStatus(status) {
447
+ if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
448
+ if (status === 429) return "RATE_LIMIT_ERROR";
449
+ if (status === 404) return "MODEL_NOT_FOUND";
450
+ if (status === 408 || status === 504) return "TIMEOUT";
451
+ if (status >= 500) return "SERVER_ERROR";
452
+ if (status >= 400) return "INVALID_REQUEST";
453
+ return "UNKNOWN_ERROR";
454
+ }
455
+ function extractMessage(body) {
456
+ if (body === null || typeof body !== "object") return void 0;
457
+ const root = body;
458
+ const error = root.error ?? root;
459
+ const message = error.message ?? error.detail ?? root.message;
460
+ return typeof message === "string" ? message : void 0;
461
+ }
462
+ function parseRetryAfter(value, now = Date.now()) {
463
+ if (!value) return void 0;
464
+ const seconds = Number(value);
465
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
466
+ const date = Date.parse(value);
467
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
468
+ }
469
+ function isTimeoutError(error) {
470
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
471
+ }
472
+ function isNetworkError(error) {
473
+ return error instanceof TypeError || typeof error?.cause === "object" && error instanceof Error && !error.code;
474
+ }
475
+ function fallbackMessage(kind, context, status) {
476
+ const { provider, operation } = context;
477
+ switch (kind) {
478
+ case "AUTHENTICATION_ERROR":
479
+ return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
480
+ case "RATE_LIMIT_ERROR":
481
+ return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
482
+ case "MODEL_NOT_FOUND":
483
+ return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
484
+ case "TIMEOUT":
485
+ return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
486
+ case "SERVER_ERROR":
487
+ return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
488
+ case "INVALID_REQUEST":
489
+ return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
490
+ default:
491
+ return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
492
+ }
493
+ }
494
+ function providerHttpError(context) {
495
+ const status = context.status ?? 0;
496
+ const kind = kindFromStatus(status);
497
+ const detail = extractMessage(context.body);
498
+ const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
499
+ return new ProviderError(
500
+ context.code ?? kindToCode(kind),
501
+ detail ?? fallbackMessage(kind, context, status),
502
+ {
503
+ cause: context.cause,
504
+ details: {
505
+ kind,
506
+ provider: context.provider,
507
+ operation: context.operation,
508
+ status,
509
+ retryable: isRetryable(kind),
510
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {},
511
+ ...detail ? { providerMessage: detail } : {}
512
+ }
513
+ }
514
+ );
515
+ }
516
+ function providerTransportError(context) {
517
+ const cause = context.cause;
518
+ const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
519
+ const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
520
+ return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
521
+ cause: context.cause,
522
+ details: {
523
+ kind,
524
+ provider: context.provider,
525
+ operation: context.operation,
526
+ retryable: isRetryable(kind)
527
+ }
528
+ });
529
+ }
530
+
411
531
  // src/providers/openai-compatible/index.ts
532
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
412
533
  var OpenAICompatibleProvider = class {
413
534
  config;
535
+ providerId;
536
+ authentication;
414
537
  log;
415
- constructor(config, log = logger) {
538
+ /** Discovered models, so getModel() answers without another round-trip. */
539
+ discovered;
540
+ env;
541
+ constructor(config, log = logger, env = process.env) {
416
542
  this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
543
+ this.env = env;
544
+ this.providerId = config.providerId ?? config.name;
545
+ this.authentication = {
546
+ method: config.apiKeyEnvVar ? "bearer" : "none",
547
+ ...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
548
+ configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
549
+ };
417
550
  this.log = log.child({ provider: config.name });
418
551
  }
419
- async listModels() {
420
- const { status, body } = await requestJson(`${this.config.baseUrl}/models`, {
421
- headers: authHeaders(this.config)
422
- });
423
- if (status === 401 || status === 403) {
424
- throw authFailed(this.config, status);
552
+ /** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
553
+ get requestTimeoutMs() {
554
+ return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
555
+ }
556
+ /** Authenticate + discover (roadmap §10, Model Discovery). */
557
+ async connect() {
558
+ authHeaders(this.config, this.env);
559
+ const models = await this.listModels();
560
+ this.discovered = models;
561
+ return models;
562
+ }
563
+ async getModel(modelId) {
564
+ const models = this.discovered ?? await this.connect();
565
+ return models.find((m) => m.id === modelId);
566
+ }
567
+ /** Raw /models body (protected so adapters can capture gateway-level metadata). */
568
+ async rawModelsResponse() {
569
+ const response = await this.modelsRequest();
570
+ return response.body;
571
+ }
572
+ async modelsRequest() {
573
+ let response;
574
+ try {
575
+ response = await requestJson(`${this.config.baseUrl}/models`, {
576
+ headers: authHeaders(this.config, this.env),
577
+ timeoutMs: this.requestTimeoutMs
578
+ });
579
+ } catch (error) {
580
+ if (error instanceof ProviderError) throw error;
581
+ throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
425
582
  }
583
+ const { status, body } = response;
426
584
  if (status !== 200) {
427
- throw unavailable(this.config, `Model discovery returned HTTP ${status}.`, status);
585
+ throw providerHttpError({
586
+ provider: this.config.name,
587
+ operation: "listModels",
588
+ status,
589
+ body
590
+ });
428
591
  }
592
+ return response;
593
+ }
594
+ async listModels() {
595
+ const body = await this.rawModelsResponse();
429
596
  const entries = extractModelEntries(body, this.config);
430
597
  const models = [];
431
598
  let skipped = 0;
@@ -434,22 +601,44 @@ var OpenAICompatibleProvider = class {
434
601
  skipped += 1;
435
602
  continue;
436
603
  }
437
- models.push({
438
- provider: this.config.name,
439
- id: entry.id,
440
- displayName: entry.id,
441
- status: "available",
442
- metadata: {
443
- ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
444
- ...typeof entry.created === "number" ? { createdAt: entry.created } : {}
445
- }
446
- });
604
+ models.push(this.toModelInfo(entry));
447
605
  }
448
606
  if (skipped > 0) {
449
607
  this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
450
608
  }
451
609
  return models;
452
610
  }
611
+ /** Maps a raw model entry onto the normalized metadata shape (§10). */
612
+ toModelInfo(entry) {
613
+ const parameters = Array.isArray(entry.supported_parameters) ? entry.supported_parameters.filter((p) => typeof p === "string") : void 0;
614
+ const contextWindow = typeof entry.context_length === "number" ? entry.context_length : typeof entry.context_window === "number" ? entry.context_window : void 0;
615
+ const inputTypes = normalizeModalities(entry.input_modalities);
616
+ const outputTypes = normalizeModalities(entry.output_modalities);
617
+ return {
618
+ provider: this.config.name,
619
+ id: entry.id,
620
+ displayName: entry.id,
621
+ ...typeof entry.name === "string" ? { name: entry.name } : {},
622
+ status: "available",
623
+ ...contextWindow !== void 0 ? { contextWindow } : {},
624
+ ...inputTypes !== void 0 ? { inputTypes } : {},
625
+ ...outputTypes !== void 0 ? { outputTypes } : {},
626
+ ...parameters !== void 0 ? {
627
+ supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
628
+ supportsStructuredOutput: parameters.includes("response_format"),
629
+ supportsStreaming: parameters.includes("stream")
630
+ } : {},
631
+ metadata: {
632
+ ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
633
+ ...typeof entry.created === "number" ? { createdAt: entry.created } : {},
634
+ ...parameters !== void 0 ? { supportedParameters: parameters } : {}
635
+ }
636
+ };
637
+ }
638
+ /** Streaming where the gateway supports it (§11). */
639
+ stream(request, onChunk) {
640
+ return streamChatCompletion(this.config, request, onChunk, this.env);
641
+ }
453
642
  async healthCheck() {
454
643
  const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
455
644
  try {
@@ -479,27 +668,35 @@ var OpenAICompatibleProvider = class {
479
668
  }
480
669
  }
481
670
  async chat(request) {
482
- const { status, body } = await requestJson(`${this.config.baseUrl}/chat/completions`, {
483
- method: "POST",
484
- headers: authHeaders(this.config),
485
- body: {
486
- model: request.model,
487
- messages: request.messages,
488
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
489
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
490
- ...request.stop !== void 0 ? { stop: request.stop } : {}
491
- }
492
- });
493
- if (status === 401 || status === 403) throw authFailed(this.config, status);
494
- if (status === 404) {
495
- throw new ProviderError(
496
- "MODEL_NOT_FOUND",
497
- `Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
498
- { details: { status } }
499
- );
671
+ let response;
672
+ try {
673
+ response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
674
+ method: "POST",
675
+ headers: authHeaders(this.config, this.env),
676
+ timeoutMs: this.requestTimeoutMs,
677
+ body: {
678
+ model: request.model,
679
+ messages: request.messages,
680
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
681
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
682
+ ...request.stop !== void 0 ? { stop: request.stop } : {}
683
+ }
684
+ });
685
+ } catch (error) {
686
+ if (error instanceof ProviderError) throw error;
687
+ throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
500
688
  }
689
+ const { status, body } = response;
501
690
  if (status !== 200) {
502
- throw unavailable(this.config, `Chat completion returned HTTP ${status}.`, status);
691
+ throw providerHttpError({
692
+ provider: this.config.name,
693
+ operation: "chat",
694
+ status,
695
+ body,
696
+ ...status === 404 ? {
697
+ code: "MODEL_NOT_FOUND"
698
+ } : {}
699
+ });
503
700
  }
504
701
  const payload = body;
505
702
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
@@ -521,6 +718,81 @@ var OpenAICompatibleProvider = class {
521
718
  };
522
719
  }
523
720
  };
721
+ async function streamChatCompletion(config, request, onChunk, env = process.env) {
722
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
723
+ const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
724
+ let response;
725
+ try {
726
+ response = await fetch(`${baseUrl}/chat/completions`, {
727
+ method: "POST",
728
+ headers: {
729
+ accept: "text/event-stream",
730
+ "content-type": "application/json",
731
+ ...authHeaders(config, env)
732
+ },
733
+ body: JSON.stringify({
734
+ model: request.model,
735
+ messages: request.messages,
736
+ stream: true,
737
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
738
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
739
+ }),
740
+ signal: AbortSignal.timeout(timeoutMs)
741
+ });
742
+ } catch (error) {
743
+ throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
744
+ }
745
+ if (!response.ok) {
746
+ const body = await response.json().catch(() => void 0);
747
+ throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
748
+ }
749
+ if (!response.body) {
750
+ throw providerHttpError({
751
+ provider: config.name,
752
+ operation: "stream",
753
+ status: 500,
754
+ body: { error: { message: "streaming is not supported by this endpoint" } }
755
+ });
756
+ }
757
+ let content = "";
758
+ let model = request.model;
759
+ let finishReason;
760
+ const decoder = new TextDecoder();
761
+ let buffer = "";
762
+ for await (const piece of response.body) {
763
+ buffer += decoder.decode(piece, { stream: true });
764
+ let newline = buffer.indexOf("\n");
765
+ while (newline >= 0) {
766
+ const line = buffer.slice(0, newline).trim();
767
+ buffer = buffer.slice(newline + 1);
768
+ newline = buffer.indexOf("\n");
769
+ if (line.length === 0 || line.startsWith(":")) continue;
770
+ const data = line.startsWith("data:") ? line.slice(5).trim() : line;
771
+ if (data === "[DONE]") continue;
772
+ let event;
773
+ try {
774
+ event = JSON.parse(data);
775
+ } catch {
776
+ continue;
777
+ }
778
+ if (typeof event.model === "string") model = event.model;
779
+ const choices = Array.isArray(event.choices) ? event.choices : [];
780
+ const first = choices[0];
781
+ const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
782
+ if (delta.length > 0) content += delta;
783
+ if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
784
+ onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
785
+ }
786
+ }
787
+ return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
788
+ }
789
+ function normalizeModalities(value) {
790
+ const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
791
+ if (!list) return void 0;
792
+ const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
793
+ const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
794
+ return modalities.length > 0 ? modalities : void 0;
795
+ }
524
796
  function normalizeBaseUrl(baseUrl) {
525
797
  return baseUrl.replace(/\/+$/, "");
526
798
  }
@@ -535,14 +807,6 @@ function extractModelEntries(body, config) {
535
807
  { details: { endpoint: "models" } }
536
808
  );
537
809
  }
538
- function authFailed(config, status) {
539
- const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
540
- return new ProviderError(
541
- "PROVIDER_AUTH_FAILED",
542
- `Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
543
- { details: { status } }
544
- );
545
- }
546
810
  function unavailable(config, message, status) {
547
811
  return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
548
812
  details: { status }
@@ -555,11 +819,33 @@ function inferHealth(error) {
555
819
 
556
820
  // src/providers/omnihilbras/index.ts
557
821
  var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
822
+ gateway;
823
+ async listModels() {
824
+ const raw = await this.rawModelsResponse();
825
+ await this.extractGatewayMetadata(raw);
826
+ const models = await super.listModels();
827
+ return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
828
+ }
558
829
  /**
559
830
  * Run the full connect flow. Throws on authentication or transport failure
560
831
  * (use healthCheck() for a non-throwing probe).
561
832
  */
562
- async connect(registry) {
833
+ /**
834
+ * Gateway metadata reported alongside the model list (gateway version, tier,
835
+ * region…), kept separate from per-model metadata (§11 — provider metadata).
836
+ */
837
+ gatewayMetadata() {
838
+ return this.gateway;
839
+ }
840
+ async extractGatewayMetadata(body) {
841
+ if (body !== null && typeof body === "object") {
842
+ const gateway = body.gateway;
843
+ if (gateway !== null && typeof gateway === "object") {
844
+ this.gateway = gateway;
845
+ }
846
+ }
847
+ }
848
+ async connectAndRegister(registry) {
563
849
  const apiKey = resolveApiKey(this.config);
564
850
  const discovered = await this.listModels();
565
851
  const seen = /* @__PURE__ */ new Set();
@@ -664,19 +950,431 @@ var ModelRegistry = class {
664
950
 
665
951
  // src/agents/process/index.ts
666
952
  import { spawn } from "child_process";
667
- var DEFAULT_TIMEOUT_MS2 = 6e5;
668
- var KILL_GRACE_MS = 5e3;
953
+
954
+ // src/agent-protocol/version.ts
955
+ var PROTOCOL_V2 = "omninode-agent-protocol/2";
956
+ var SUPPORTED_PROTOCOL_VERSIONS = [
957
+ "omninode-agent-protocol/1",
958
+ PROTOCOL_V2
959
+ ];
960
+ var MAX_MESSAGE_BYTES = 1048576;
961
+ function isSupportedProtocol(protocol) {
962
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
963
+ }
964
+
965
+ // src/agent-protocol/types.ts
966
+ function newMessageId(prefix = "msg") {
967
+ const random = Math.random().toString(36).slice(2, 10);
968
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
969
+ }
970
+ function envelope(type, payload, ids = {}) {
971
+ return {
972
+ protocol: PROTOCOL_V2,
973
+ messageId: newMessageId(),
974
+ ...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
975
+ ...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
976
+ ...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
977
+ ...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
978
+ ...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
979
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
980
+ type,
981
+ payload,
982
+ ...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
983
+ };
984
+ }
985
+
986
+ // src/agent-protocol/codec.ts
987
+ var KNOWN_TYPES = /* @__PURE__ */ new Set([
988
+ // node → agent
989
+ "TASK",
990
+ "HELLO_ACK",
991
+ "CONTEXT",
992
+ "INSTRUCTION",
993
+ "RESPONSE",
994
+ "CANCEL",
995
+ // agent → node
996
+ "HELLO",
997
+ "TASK_REQUEST",
998
+ "TASK_ACCEPTED",
999
+ "TASK_STARTED",
1000
+ "STATUS",
1001
+ "QUESTION",
1002
+ "REPORT",
1003
+ "ARTIFACT",
1004
+ "COMPLETION",
1005
+ "ERROR",
1006
+ "CANCELLED"
1007
+ ]);
1008
+ var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
1009
+ function validateEnvelope(value, line) {
1010
+ const sample = line?.slice(0, 200);
1011
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1012
+ return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
1013
+ }
1014
+ const raw = value;
1015
+ const protocol = raw.protocol;
1016
+ if (protocol === void 0) {
1017
+ const type = raw.type;
1018
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
1019
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
1020
+ }
1021
+ return {
1022
+ ok: true,
1023
+ legacy: true,
1024
+ envelope: {
1025
+ protocol: "omninode-agent-protocol/1",
1026
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
1027
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1028
+ type,
1029
+ payload: raw.payload
1030
+ }
1031
+ };
1032
+ }
1033
+ if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
1034
+ return {
1035
+ ok: false,
1036
+ error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
1037
+ };
1038
+ }
1039
+ if (raw.type === void 0) {
1040
+ return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
1041
+ }
1042
+ if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
1043
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
1044
+ }
1045
+ if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
1046
+ return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
1047
+ }
1048
+ if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
1049
+ return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
1050
+ }
1051
+ return {
1052
+ ok: true,
1053
+ legacy: protocol !== PROTOCOL_V2,
1054
+ envelope: {
1055
+ protocol,
1056
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
1057
+ ...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
1058
+ ...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
1059
+ ...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
1060
+ ...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
1061
+ ...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
1062
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1063
+ type: raw.type,
1064
+ payload: raw.payload,
1065
+ ...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
1066
+ }
1067
+ };
1068
+ }
1069
+ function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
1070
+ const trimmed = line.trim();
1071
+ if (trimmed.length === 0) {
1072
+ return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
1073
+ }
1074
+ if (trimmed.length > maxBytes) {
1075
+ return {
1076
+ ok: false,
1077
+ error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
1078
+ };
1079
+ }
1080
+ let value;
1081
+ try {
1082
+ value = JSON.parse(trimmed);
1083
+ } catch (error) {
1084
+ return {
1085
+ ok: false,
1086
+ error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
1087
+ };
1088
+ }
1089
+ return validateEnvelope(value, trimmed);
1090
+ }
1091
+ function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
1092
+ const line = JSON.stringify(envelope2);
1093
+ if (line.length > maxBytes) {
1094
+ throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
1095
+ }
1096
+ return line;
1097
+ }
1098
+ var ProtocolDecoder = class {
1099
+ buffer = "";
1100
+ maxBytes;
1101
+ constructor(maxBytes = MAX_MESSAGE_BYTES) {
1102
+ this.maxBytes = maxBytes;
1103
+ }
1104
+ push(chunk) {
1105
+ this.buffer += chunk;
1106
+ const results = [];
1107
+ let index = this.buffer.indexOf("\n");
1108
+ while (index >= 0) {
1109
+ const line = this.buffer.slice(0, index);
1110
+ this.buffer = this.buffer.slice(index + 1);
1111
+ if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
1112
+ index = this.buffer.indexOf("\n");
1113
+ }
1114
+ if (this.buffer.length > this.maxBytes) {
1115
+ results.push({
1116
+ ok: false,
1117
+ error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
1118
+ });
1119
+ this.buffer = "";
1120
+ }
1121
+ return results;
1122
+ }
1123
+ /** Flush any trailing line when the process exits. */
1124
+ flush() {
1125
+ if (this.buffer.trim().length === 0) {
1126
+ this.buffer = "";
1127
+ return [];
1128
+ }
1129
+ const result = decodeMessage(this.buffer, this.maxBytes);
1130
+ this.buffer = "";
1131
+ return [result];
1132
+ }
1133
+ };
1134
+
1135
+ // src/agent-protocol/handshake.ts
1136
+ function parseHello(envelopeValue) {
1137
+ if (envelopeValue.type !== "HELLO") {
1138
+ return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
1139
+ }
1140
+ const payload = envelopeValue.payload;
1141
+ if (payload === null || typeof payload !== "object") {
1142
+ return { ok: false, reason: "HELLO payload must be an object" };
1143
+ }
1144
+ const p = payload;
1145
+ if (typeof p.name !== "string" || p.name.length === 0) {
1146
+ return { ok: false, reason: "HELLO payload is missing 'name'" };
1147
+ }
1148
+ return {
1149
+ ok: true,
1150
+ descriptor: {
1151
+ protocol: envelopeValue.protocol,
1152
+ name: p.name,
1153
+ ...typeof p.version === "string" ? { version: p.version } : {},
1154
+ capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
1155
+ supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
1156
+ }
1157
+ };
1158
+ }
1159
+ function buildHello(descriptor) {
1160
+ return envelope("HELLO", descriptor, { agentId: descriptor.name });
1161
+ }
1162
+ function buildHelloAck(descriptor) {
1163
+ return envelope("HELLO_ACK", {
1164
+ ok: true,
1165
+ protocol: PROTOCOL_V2,
1166
+ agent: descriptor.name,
1167
+ ...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
1168
+ supportedProtocols: [PROTOCOL_V2]
1169
+ }, { agentId: descriptor.name });
1170
+ }
1171
+
1172
+ // src/agent-protocol/session.ts
1173
+ var ProtocolSession = class {
1174
+ decoder;
1175
+ ids;
1176
+ responder;
1177
+ violations = [];
1178
+ questions = [];
1179
+ artifacts = [];
1180
+ reports = [];
1181
+ descriptor;
1182
+ completionSummary;
1183
+ errorMessage;
1184
+ constructor(ids, options = {}) {
1185
+ this.ids = ids;
1186
+ this.responder = options.responder;
1187
+ this.decoder = new ProtocolDecoder(options.maxBytes);
1188
+ }
1189
+ /** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
1190
+ taskMessages(objective, role, context, instruction) {
1191
+ const ids = this.ids;
1192
+ const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
1193
+ if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
1194
+ if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
1195
+ if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
1196
+ return messages.map((m) => encodeMessage(m));
1197
+ }
1198
+ helloAck(descriptor) {
1199
+ return encodeMessage(buildHelloAck(descriptor));
1200
+ }
1201
+ /** Consumes decoded stdout, answering questions as they arrive. */
1202
+ async handle(decoded) {
1203
+ if (!decoded.ok) {
1204
+ this.violations.push(decoded.error);
1205
+ return;
1206
+ }
1207
+ const message = decoded.envelope;
1208
+ if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
1209
+ if (message.type === "HELLO") {
1210
+ const hello = parseHello(message);
1211
+ if (hello.ok) this.descriptor = hello.descriptor;
1212
+ else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
1213
+ return;
1214
+ }
1215
+ if (message.type === "QUESTION") {
1216
+ await this.answerQuestion(message);
1217
+ return;
1218
+ }
1219
+ if (message.type === "REPORT") {
1220
+ this.reports.push(message.payload);
1221
+ return;
1222
+ }
1223
+ if (message.type === "ARTIFACT") {
1224
+ this.artifacts.push(message.payload);
1225
+ return;
1226
+ }
1227
+ if (message.type === "COMPLETION") {
1228
+ const payload = message.payload;
1229
+ this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
1230
+ return;
1231
+ }
1232
+ if (message.type === "ERROR") {
1233
+ const payload = message.payload;
1234
+ const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
1235
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1236
+ this.errorMessage = `Agent error${code}: ${detail}`;
1237
+ return;
1238
+ }
1239
+ if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
1240
+ return;
1241
+ }
1242
+ this.violations.push({
1243
+ code: "UNKNOWN_TYPE",
1244
+ message: `unexpected message type ${message.type} from agent`
1245
+ });
1246
+ }
1247
+ async answerQuestion(message) {
1248
+ const payload = message.payload ?? {};
1249
+ const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
1250
+ const question = typeof payload.question === "string" ? payload.question : "";
1251
+ const expected = typeof payload.expected === "string" ? payload.expected : "text";
1252
+ let answer;
1253
+ if (this.responder) {
1254
+ try {
1255
+ answer = await this.responder({ questionId, question, expected });
1256
+ } catch (error) {
1257
+ this.violations.push({
1258
+ code: "INVALID_PAYLOAD",
1259
+ message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
1260
+ });
1261
+ }
1262
+ }
1263
+ this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
1264
+ this.pendingAnswers.push(
1265
+ encodeMessage(
1266
+ envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
1267
+ ...this.ids,
1268
+ requestId: message.requestId ?? this.ids.requestId
1269
+ })
1270
+ )
1271
+ );
1272
+ }
1273
+ pendingAnswers = [];
1274
+ /** True once any inbound message carried the v2 protocol field. */
1275
+ sawV2 = false;
1276
+ };
1277
+ async function decodeStream(session, chunk) {
1278
+ const decoded = session.decoder.push(chunk);
1279
+ for (const result of decoded) {
1280
+ await session.handle(result);
1281
+ }
1282
+ return session.pendingAnswers.splice(0);
1283
+ }
1284
+ function requireCompletion(session) {
1285
+ if (session.errorMessage !== void 0) {
1286
+ throw new ProtocolError(session.errorMessage);
1287
+ }
1288
+ }
1289
+
1290
+ // src/agents/cwd.ts
1291
+ import { statSync } from "fs";
1292
+ import { isAbsolute, relative, resolve } from "path";
1293
+ function validateWorkingDirectory(cwd, options = {}) {
1294
+ if (!cwd) return void 0;
1295
+ const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
1296
+ let stats;
1297
+ try {
1298
+ stats = statSync(normalized);
1299
+ } catch (error) {
1300
+ throw new AgentError(
1301
+ "AGENT_NOT_FOUND",
1302
+ `Working directory "${normalized}" does not exist or is not accessible.`,
1303
+ { cause: error, details: { cwd: normalized } }
1304
+ );
1305
+ }
1306
+ if (!stats.isDirectory()) {
1307
+ throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
1308
+ details: { cwd: normalized }
1309
+ });
1310
+ }
1311
+ try {
1312
+ statSync(resolve(normalized, "."));
1313
+ } catch (error) {
1314
+ throw new AgentError(
1315
+ "AGENT_NOT_FOUND",
1316
+ `Working directory "${normalized}" is not accessible.`,
1317
+ { cause: error }
1318
+ );
1319
+ }
1320
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
1321
+ const rel = relative(projectRoot, normalized);
1322
+ const outsideProject = rel.startsWith("..") || isAbsolute(rel);
1323
+ if (outsideProject && options.allowExternal !== true) {
1324
+ throw new AgentError(
1325
+ "AGENT_NOT_FOUND",
1326
+ `Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
1327
+ { details: { cwd: normalized, projectRoot } }
1328
+ );
1329
+ }
1330
+ return { cwd: normalized, outsideProject };
1331
+ }
1332
+
1333
+ // src/agents/env.ts
1334
+ var BASELINE_KEYS = ["PATH", "HOME"];
1335
+ function resolveEnvPolicy(config) {
1336
+ if (config.envPolicy !== void 0) return config.envPolicy;
1337
+ if (config.inheritEnv === false) return "explicit";
1338
+ return "inherit";
1339
+ }
669
1340
  function buildChildEnv(config) {
670
1341
  const configured = config.env ?? {};
671
- if (config.inheritEnv !== false) {
1342
+ const policy = resolveEnvPolicy(config);
1343
+ if (policy === "inherit") {
672
1344
  return { ...process.env, ...configured };
673
1345
  }
674
- return {
675
- PATH: process.env.PATH ?? "",
676
- HOME: process.env.HOME ?? "",
677
- ...configured
678
- };
1346
+ const base = {};
1347
+ for (const key of BASELINE_KEYS) {
1348
+ const value = process.env[key];
1349
+ if (value !== void 0) base[key] = value;
1350
+ }
1351
+ switch (policy) {
1352
+ case "explicit":
1353
+ return { ...base, ...configured };
1354
+ case "allowlist": {
1355
+ const allowed = { ...base };
1356
+ for (const key of config.envAllowlist ?? []) {
1357
+ const value = process.env[key];
1358
+ if (value !== void 0) allowed[key] = value;
1359
+ }
1360
+ return { ...allowed, ...configured };
1361
+ }
1362
+ case "denylist": {
1363
+ const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
1364
+ const filtered = {};
1365
+ for (const [key, value] of Object.entries(process.env)) {
1366
+ if (denied.has(key.toUpperCase())) continue;
1367
+ filtered[key] = value;
1368
+ }
1369
+ return { ...filtered, ...configured };
1370
+ }
1371
+ }
679
1372
  }
1373
+
1374
+ // src/agents/process/index.ts
1375
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
1376
+ var KILL_GRACE_MS = 5e3;
1377
+ var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
680
1378
  function composeTaskText(input) {
681
1379
  const sections = [`# Objective
682
1380
  ${input.objective}`];
@@ -697,18 +1395,19 @@ ${input.instruction}`);
697
1395
  return sections.join("\n\n");
698
1396
  }
699
1397
  function buildProtocolMessages(input, agentName) {
700
- const meta = {
1398
+ const session = new ProtocolSession({
1399
+ requestId: `req-${input.taskId}`,
701
1400
  taskId: input.taskId,
702
- agent: agentName,
703
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
704
- };
705
- const messages = [
706
- { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
707
- ];
708
- if (input.role) messages.push({ type: "ROLE", payload: input.role });
709
- if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
710
- if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
711
- return messages.map((message) => JSON.stringify({ meta, message }));
1401
+ agentId: agentName,
1402
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1403
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1404
+ });
1405
+ return session.taskMessages(
1406
+ input.objective,
1407
+ input.role,
1408
+ input.context,
1409
+ input.instruction
1410
+ );
712
1411
  }
713
1412
  var ProcessAgent = class {
714
1413
  config;
@@ -737,40 +1436,73 @@ var ProcessAgent = class {
737
1436
  }
738
1437
  const mode = this.config.inputMode ?? "stdin";
739
1438
  const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
740
- return new Promise((resolve, reject) => {
1439
+ return new Promise((resolve2, reject) => {
1440
+ let cwd;
1441
+ try {
1442
+ cwd = validateWorkingDirectory(this.config.cwd, {
1443
+ allowExternal: this.config.allowExternalCwd
1444
+ })?.cwd;
1445
+ } catch (error) {
1446
+ reject(error);
1447
+ return;
1448
+ }
741
1449
  const child = spawn(
742
1450
  command,
743
1451
  [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
744
1452
  {
745
- cwd: this.config.cwd,
1453
+ ...cwd !== void 0 ? { cwd } : {},
746
1454
  env: buildChildEnv(this.config),
747
- stdio: ["pipe", "pipe", "pipe"]
1455
+ stdio: ["pipe", "pipe", "pipe"],
1456
+ // Own process group so the whole tree can be terminated together
1457
+ // (roadmap §8: no orphaned children).
1458
+ ...process.platform === "win32" ? {} : { detached: true }
748
1459
  }
749
1460
  );
750
1461
  this.running.set(input.taskId, child);
1462
+ GLOBAL_RUNNING.add(child);
1463
+ installSignalForwarding();
751
1464
  let stdout = "";
752
1465
  let stderr = "";
753
1466
  let timedOut = false;
1467
+ let outputLimitExceeded = false;
754
1468
  let settled = false;
1469
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
755
1470
  const timer = setTimeout(() => {
756
1471
  timedOut = true;
757
- this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
758
- child.kill("SIGTERM");
759
- setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
1472
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
1473
+ killTree(child, "SIGTERM");
1474
+ setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
760
1475
  }, timeoutMs);
761
1476
  const settle = (finish) => {
762
1477
  if (settled) return;
763
1478
  settled = true;
764
1479
  clearTimeout(timer);
765
1480
  this.running.delete(input.taskId);
1481
+ GLOBAL_RUNNING.delete(child);
766
1482
  finish();
767
1483
  };
768
- child.stdout?.on("data", (chunk) => {
769
- stdout += chunk.toString();
770
- });
771
- child.stderr?.on("data", (chunk) => {
772
- stderr += chunk.toString();
773
- });
1484
+ const collect = (into, chunk) => {
1485
+ if (outputLimitExceeded) return;
1486
+ const text = sanitizeChunk(chunk);
1487
+ if (into === "stdout") {
1488
+ if (stdout.length + text.length > maxOutputBytes) {
1489
+ stdout = (stdout + text).slice(0, maxOutputBytes);
1490
+ outputLimitExceeded = true;
1491
+ this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
1492
+ killTree(child, "SIGKILL");
1493
+ return;
1494
+ }
1495
+ stdout += text;
1496
+ } else {
1497
+ if (stderr.length + text.length > maxOutputBytes) {
1498
+ stderr = (stderr + text).slice(0, maxOutputBytes);
1499
+ return;
1500
+ }
1501
+ stderr += text;
1502
+ }
1503
+ };
1504
+ child.stdout?.on("data", (chunk) => collect("stdout", chunk));
1505
+ child.stderr?.on("data", (chunk) => collect("stderr", chunk));
774
1506
  child.stdin?.on("error", () => {
775
1507
  });
776
1508
  if (mode !== "arg") {
@@ -804,7 +1536,19 @@ var ProcessAgent = class {
804
1536
  new AgentError(
805
1537
  "AGENT_TIMEOUT",
806
1538
  `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
807
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
1539
+ {
1540
+ details: {
1541
+ operation: "agent.run",
1542
+ agent: this.config.name,
1543
+ durationMs: timeoutMs,
1544
+ // The process was started and may have performed side
1545
+ // effects before being killed — the outcome is unknown,
1546
+ // not failed (roadmap §6.3).
1547
+ dispatched: true,
1548
+ stdout: stdout.slice(0, 4e3),
1549
+ stderr: stderr.slice(0, 2e3)
1550
+ }
1551
+ }
808
1552
  )
809
1553
  );
810
1554
  return;
@@ -812,7 +1556,7 @@ var ProcessAgent = class {
812
1556
  const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
813
1557
  if (mode === "protocol") {
814
1558
  try {
815
- resolve(
1559
+ resolve2(
816
1560
  interpretProtocolRun({
817
1561
  input,
818
1562
  agentName: this.config.name,
@@ -827,14 +1571,14 @@ var ProcessAgent = class {
827
1571
  return;
828
1572
  }
829
1573
  const trimmed = stdout.trim();
830
- resolve({
1574
+ resolve2({
831
1575
  taskId: input.taskId,
832
- status: code === 0 ? "completed" : "failed",
1576
+ status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
833
1577
  summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
834
1578
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
835
1579
  exitCode: code ?? void 0,
836
1580
  logs,
837
- error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
1581
+ ...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
838
1582
  });
839
1583
  });
840
1584
  });
@@ -843,67 +1587,117 @@ var ProcessAgent = class {
843
1587
  /** Ask the agent to stop current work (SIGTERM). */
844
1588
  async cancel(taskId) {
845
1589
  const child = this.running.get(taskId);
846
- if (child) child.kill("SIGTERM");
1590
+ if (child) killTree(child, "SIGTERM");
847
1591
  }
848
1592
  runningTaskIds() {
849
1593
  return [...this.running.keys()];
850
1594
  }
851
1595
  };
852
- function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
853
- const reports = [];
854
- let summary;
855
- let error;
856
- let sawProtocolMessage = false;
857
- for (const line of stdout.split("\n")) {
858
- const trimmed2 = line.trim();
859
- if (trimmed2.length === 0) continue;
860
- const message = parseAgentMessage(trimmed2);
861
- if (!message) continue;
862
- sawProtocolMessage = true;
863
- if (message.type === "REPORT") {
864
- const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
865
- if (report) reports.push(report);
866
- } else if (message.type === "COMPLETION") {
867
- const payload = message.payload;
868
- if (typeof payload?.summary === "string") summary = payload.summary;
869
- } else if (message.type === "ERROR") {
870
- const payload = message.payload;
871
- const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
872
- const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
873
- error = `Agent error${code}: ${detail}`;
874
- }
1596
+ function interpretProtocolRun({
1597
+ input,
1598
+ agentName,
1599
+ stdout,
1600
+ exitCode,
1601
+ logs,
1602
+ responder
1603
+ }) {
1604
+ const session = new ProtocolSession(
1605
+ {
1606
+ requestId: `req-${input.taskId}`,
1607
+ taskId: input.taskId,
1608
+ agentId: agentName,
1609
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1610
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1611
+ },
1612
+ { ...responder !== void 0 ? { responder } : {} }
1613
+ );
1614
+ for (const result of session.decoder.push(stdout)) {
1615
+ session.handle(result);
875
1616
  }
876
- if (!sawProtocolMessage && exitCode === 0) {
877
- throw new ProtocolError(
878
- `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
879
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
880
- );
1617
+ for (const result of session.decoder.flush()) {
1618
+ session.handle(result);
881
1619
  }
1620
+ const reports = session.reports.flatMap((payload) => Array.isArray(payload) ? payload : [payload]).map((payload, index) => normalizeReport(payload, input.taskId, agentName, index)).filter((report) => report !== void 0);
882
1621
  const trimmed = stdout.trim();
1622
+ const protocolBlock = {
1623
+ protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
1624
+ legacy: !session.sawV2 && session.descriptor === void 0,
1625
+ ...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
1626
+ questions: session.questions,
1627
+ violations: session.violations.map((v) => `${v.code}: ${v.message}`)
1628
+ };
1629
+ if (session.errorMessage !== void 0) {
1630
+ return {
1631
+ taskId: input.taskId,
1632
+ status: "failed",
1633
+ error: session.errorMessage,
1634
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1635
+ exitCode: exitCode ?? void 0,
1636
+ logs,
1637
+ protocol: protocolBlock,
1638
+ ...reports.length > 0 ? { reports } : {}
1639
+ };
1640
+ }
1641
+ if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
1642
+ return {
1643
+ taskId: input.taskId,
1644
+ status: "failed",
1645
+ error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
1646
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1647
+ exitCode: exitCode ?? void 0,
1648
+ logs,
1649
+ protocol: protocolBlock
1650
+ };
1651
+ }
1652
+ const completed = exitCode === 0 && session.completionSummary !== void 0;
883
1653
  return {
884
1654
  taskId: input.taskId,
885
- status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
886
- summary: summary ?? reports[0]?.summary,
1655
+ status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
1656
+ summary: session.completionSummary ?? reports[0]?.summary,
887
1657
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
888
1658
  exitCode: exitCode ?? void 0,
889
1659
  logs,
890
- reports: reports.length > 0 ? reports : void 0,
891
- error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1660
+ protocol: protocolBlock,
1661
+ ...reports.length > 0 ? { reports } : {},
1662
+ ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
892
1663
  };
893
1664
  }
894
- function parseAgentMessage(line) {
895
- try {
896
- const parsed = JSON.parse(line);
897
- if (typeof parsed.type === "string") {
898
- return { type: parsed.type, payload: parsed.payload };
899
- }
900
- const inner = parsed.message;
901
- if (inner && typeof inner.type === "string") {
902
- return { type: inner.type, payload: inner.payload };
1665
+ var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
1666
+ var signalForwardingInstalled = false;
1667
+ function installSignalForwarding() {
1668
+ if (signalForwardingInstalled) return;
1669
+ signalForwardingInstalled = true;
1670
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1671
+ process.on(signal, () => {
1672
+ for (const child of GLOBAL_RUNNING) {
1673
+ killTree(child, "SIGTERM");
1674
+ }
1675
+ });
1676
+ }
1677
+ }
1678
+ function killTree(child, signal) {
1679
+ if (child.pid === void 0) return;
1680
+ if (process.platform !== "win32") {
1681
+ try {
1682
+ process.kill(-child.pid, signal);
1683
+ return;
1684
+ } catch {
903
1685
  }
1686
+ }
1687
+ try {
1688
+ child.kill(signal);
904
1689
  } catch {
905
1690
  }
906
- return void 0;
1691
+ }
1692
+ function describeExit(code, signal) {
1693
+ if (signal !== null) {
1694
+ return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
1695
+ }
1696
+ if (code === null) return "Process terminated without an exit code.";
1697
+ return `Process exited with code ${code}.`;
1698
+ }
1699
+ function sanitizeChunk(chunk) {
1700
+ return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
907
1701
  }
908
1702
  function normalizeReport(payload, taskId, agentName, index) {
909
1703
  if (payload === null || typeof payload !== "object") return void 0;
@@ -924,6 +1718,40 @@ function firstLine(text, maxLength = 200) {
924
1718
  return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
925
1719
  }
926
1720
 
1721
+ // src/agents/adapters.ts
1722
+ var AgentAdapterRegistry = class {
1723
+ adapters = /* @__PURE__ */ new Map();
1724
+ register(adapter) {
1725
+ this.adapters.set(adapter.integration, adapter);
1726
+ }
1727
+ get(integration) {
1728
+ return this.adapters.get(integration);
1729
+ }
1730
+ list() {
1731
+ return [...this.adapters.values()];
1732
+ }
1733
+ create(config) {
1734
+ const adapter = this.get(config.integration);
1735
+ if (!adapter) {
1736
+ throw new AgentError(
1737
+ "AGENT_NOT_FOUND",
1738
+ `No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
1739
+ );
1740
+ }
1741
+ return adapter.create(config);
1742
+ }
1743
+ };
1744
+ var ProcessAgentAdapter = class {
1745
+ constructor(factory) {
1746
+ this.factory = factory;
1747
+ }
1748
+ factory;
1749
+ integration = "process";
1750
+ create(config) {
1751
+ return this.factory(config);
1752
+ }
1753
+ };
1754
+
927
1755
  // src/agents/registry.ts
928
1756
  var AgentRegistry = class {
929
1757
  agents = /* @__PURE__ */ new Map();
@@ -939,14 +1767,15 @@ var AgentRegistry = class {
939
1767
  };
940
1768
 
941
1769
  // src/agents/factory.ts
942
- function createAgent(config) {
943
- if (config.integration === "process") {
944
- return new ProcessAgent(config);
945
- }
1770
+ var defaultAgentAdapters = new AgentAdapterRegistry();
1771
+ defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
1772
+ function createAgent(config, adapters = defaultAgentAdapters) {
1773
+ const registered = adapters.get(config.integration);
1774
+ if (registered) return registered.create(config);
946
1775
  if (config.integration === "native") {
947
1776
  throw new OmniNodeError(
948
1777
  "NOT_IMPLEMENTED",
949
- `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1778
+ `No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
950
1779
  );
951
1780
  }
952
1781
  throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
@@ -1082,12 +1911,18 @@ import { randomBytes } from "crypto";
1082
1911
  var ALLOWED_TRANSITIONS = {
1083
1912
  created: ["queued", "running", "cancelled"],
1084
1913
  queued: ["running", "cancelled"],
1085
- running: ["completed", "failed", "cancelled"],
1914
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
1086
1915
  waiting: ["running", "cancelled"],
1087
1916
  completed: [],
1088
- failed: [],
1089
- cancelled: []
1917
+ // failed → running is the only re-entry: bounded automatic retry (§6.4).
1918
+ failed: ["running"],
1919
+ timed_out: [],
1920
+ cancelled: [],
1921
+ unknown: [],
1922
+ partially_completed: []
1090
1923
  };
1924
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
1925
+ var EXECUTION_HISTORY_LIMIT = 10;
1091
1926
  var TaskEngine = class {
1092
1927
  constructor(options) {
1093
1928
  this.options = options;
@@ -1119,6 +1954,7 @@ var TaskEngine = class {
1119
1954
  id: generateTaskId(),
1120
1955
  objective: input.objective.trim(),
1121
1956
  status: "created",
1957
+ attempt: 0,
1122
1958
  createdAt: now,
1123
1959
  updatedAt: now,
1124
1960
  ...input.project !== void 0 ? { project: input.project } : {},
@@ -1135,8 +1971,10 @@ var TaskEngine = class {
1135
1971
  const task = await this.mustGet(id);
1136
1972
  return this.transition(task, "queued");
1137
1973
  }
1138
- /** Runs the task through its assigned agent and records the result. */
1139
- async run(id) {
1974
+ /** Runs the task through its assigned agent and records the result.
1975
+ * `maxAttempts` enables bounded automatic retries of *retryable* errors
1976
+ * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
1977
+ async run(id, options = {}) {
1140
1978
  const task = await this.mustGet(id);
1141
1979
  if (task.status !== "created" && task.status !== "queued") {
1142
1980
  throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
@@ -1149,49 +1987,89 @@ var TaskEngine = class {
1149
1987
  throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1150
1988
  }
1151
1989
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1152
- const running = await this.transition(task, "running");
1153
1990
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1154
- let background = running.context?.background;
1991
+ let background = task.context?.background;
1155
1992
  if (this.options.memory) {
1156
1993
  const memoryContext = await this.options.memory.gatherContext({
1157
- objective: running.objective,
1158
- ...running.role ? { role: running.role } : {},
1159
- ...running.project ? { project: running.project } : {}
1994
+ objective: task.objective,
1995
+ ...task.role ? { role: task.role } : {},
1996
+ ...task.project ? { project: task.project } : {}
1160
1997
  });
1161
1998
  if (memoryContext.length > 0) {
1162
1999
  background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1163
2000
  }
1164
2001
  }
1165
- let output;
1166
- try {
1167
- output = await agent.run({
1168
- taskId: running.id,
1169
- objective: running.objective,
1170
- ...role ? { role } : {},
1171
- ...background !== void 0 ? { context: background } : {}
1172
- });
1173
- } catch (error) {
1174
- const message = error instanceof Error ? error.message : String(error);
1175
- this.log.error(`Task ${task.id} failed: ${message}`);
1176
- return this.transition(running, "failed", {
1177
- result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
2002
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
2003
+ const backoffMs = options.retryBackoffMs ?? 0;
2004
+ let current = task;
2005
+ for (let i = 0; i < maxAttempts; i += 1) {
2006
+ const attempt = current.attempt + 1;
2007
+ const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
2008
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2009
+ current = await this.transition(current, "running", { attempt });
2010
+ let output;
2011
+ let thrown;
2012
+ try {
2013
+ output = await agent.run({
2014
+ taskId: current.id,
2015
+ objective: current.objective,
2016
+ ...role ? { role } : {},
2017
+ ...background !== void 0 ? { context: background } : {}
2018
+ });
2019
+ } catch (error) {
2020
+ thrown = error;
2021
+ }
2022
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2023
+ if (thrown !== void 0 || output === void 0) {
2024
+ const outcome = classifyThrown(thrown);
2025
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
2026
+ const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
2027
+ current = await this.transition(current, outcome, {
2028
+ attempt,
2029
+ lastError: message,
2030
+ execution: {
2031
+ executionId,
2032
+ attempt,
2033
+ startedAt,
2034
+ finishedAt,
2035
+ outcome,
2036
+ error: message
2037
+ }
2038
+ });
2039
+ this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
2040
+ if (retryable) {
2041
+ if (backoffMs > 0) await sleep(backoffMs * attempt);
2042
+ continue;
2043
+ }
2044
+ if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
2045
+ return current;
2046
+ }
2047
+ const currentState = await this.options.store.get(current.id);
2048
+ if (currentState?.status === "cancelled") {
2049
+ current = currentState;
2050
+ break;
2051
+ }
2052
+ const result = {
2053
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
2054
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
2055
+ // Keep the verbatim output (bounded) for report extraction and audit.
2056
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
2057
+ ...output.error !== void 0 ? { error: output.error } : {},
2058
+ finishedAt
2059
+ };
2060
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
2061
+ current = await this.transition(current, finalStatus, {
2062
+ attempt,
2063
+ result,
2064
+ execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
1178
2065
  });
2066
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
2067
+ break;
1179
2068
  }
1180
- const result = {
1181
- ...output.summary !== void 0 ? { summary: output.summary } : {},
1182
- ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1183
- // Keep the verbatim output (bounded) for report extraction and audit.
1184
- ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1185
- ...output.error !== void 0 ? { error: output.error } : {},
1186
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1187
- };
1188
- const finalStatus = output.status === "completed" ? "completed" : "failed";
1189
- const finished = await this.transition(running, finalStatus, { result });
1190
- this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1191
2069
  if (this.options.memory) {
1192
- await this.options.memory.recordTaskOutcome(finished);
2070
+ await this.options.memory.recordTaskOutcome(current);
1193
2071
  }
1194
- return finished;
2072
+ return current;
1195
2073
  }
1196
2074
  async cancel(id) {
1197
2075
  const task = await this.mustGet(id);
@@ -1204,13 +2082,14 @@ var TaskEngine = class {
1204
2082
  }
1205
2083
  return this.transition(task, "cancelled");
1206
2084
  }
1207
- /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
2085
+ /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
1208
2086
  async retry(id) {
1209
2087
  const task = await this.mustGet(id);
1210
- if (task.status !== "failed" && task.status !== "cancelled") {
2088
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
2089
+ if (!retryableStates.includes(task.status)) {
1211
2090
  throw new TaskError(
1212
2091
  "TASK_INVALID",
1213
- `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
2092
+ `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1214
2093
  );
1215
2094
  }
1216
2095
  const { result: _discarded, ...rest } = task;
@@ -1242,10 +2121,14 @@ var TaskEngine = class {
1242
2121
  if (!allowed.includes(to)) {
1243
2122
  throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1244
2123
  }
2124
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
1245
2125
  const updated = {
1246
2126
  ...task,
1247
2127
  status: to,
1248
2128
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2129
+ ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
2130
+ ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
2131
+ ...executions !== void 0 ? { executions } : {},
1249
2132
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1250
2133
  };
1251
2134
  await this.options.store.save(updated);
@@ -1266,6 +2149,19 @@ var TaskEngine = class {
1266
2149
  function generateTaskId() {
1267
2150
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1268
2151
  }
2152
+ function errorCode(error) {
2153
+ return error?.code ?? "";
2154
+ }
2155
+ function classifyThrown(error) {
2156
+ if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
2157
+ const dispatched = error.details?.dispatched;
2158
+ return dispatched === false ? "timed_out" : "unknown";
2159
+ }
2160
+ return "failed";
2161
+ }
2162
+ function sleep(ms) {
2163
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2164
+ }
1269
2165
 
1270
2166
  // src/audit/index.ts
1271
2167
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
@@ -1308,6 +2204,23 @@ var FileAuditLog = class {
1308
2204
  }
1309
2205
  };
1310
2206
 
2207
+ // src/memory/base.ts
2208
+ function defaultSearch(provider, text, options = {}) {
2209
+ return provider.retrieve({
2210
+ text,
2211
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2212
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2213
+ });
2214
+ }
2215
+ function defaultMetadata(name, options = {}) {
2216
+ return {
2217
+ name,
2218
+ capabilities: options.capabilities ?? ["retrieve", "store", "search"],
2219
+ relevanceRanking: options.relevanceRanking ?? false,
2220
+ ...options.backend !== void 0 ? { backend: options.backend } : {}
2221
+ };
2222
+ }
2223
+
1311
2224
  // src/memory/local.ts
1312
2225
  var STOPWORDS = /* @__PURE__ */ new Set([
1313
2226
  "the",
@@ -1370,9 +2283,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
1370
2283
  constructor(directory) {
1371
2284
  super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
1372
2285
  }
1373
- async query(query) {
2286
+ metadata() {
2287
+ return defaultMetadata(this.name, {
2288
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2289
+ relevanceRanking: true,
2290
+ backend: { kind: "json-file", deterministicScoring: true }
2291
+ });
2292
+ }
2293
+ search(text, options = {}) {
2294
+ return defaultSearch(this, text, options);
2295
+ }
2296
+ /** @deprecated v1 alias of {@link retrieve}. */
2297
+ query(query) {
2298
+ return this.retrieve(query);
2299
+ }
2300
+ /** @deprecated v1 alias of {@link store}. */
2301
+ write(entry) {
2302
+ return this.store(entry);
2303
+ }
2304
+ async retrieve(query) {
1374
2305
  const entries = await this.readAll();
1375
- const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter(
2306
+ const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter((entry) => query.category ? entry.category === query.category : true).filter(
1376
2307
  (entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
1377
2308
  );
1378
2309
  if (!query.text || query.text.trim().length === 0) {
@@ -1381,7 +2312,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
1381
2312
  const queryKeywords = keywords(query.text);
1382
2313
  return filtered.map((entry) => ({ entry, score: relevanceScore(queryKeywords, entry) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry }) => entry);
1383
2314
  }
1384
- write(entry) {
2315
+ store(entry) {
1385
2316
  return this.mutate((entries) => {
1386
2317
  const index = entries.findIndex((e) => e.key === entry.key);
1387
2318
  const stamped = {
@@ -1412,11 +2343,40 @@ var RememberaMemoryProvider = class {
1412
2343
  this.env = config.env ?? process.env;
1413
2344
  this.log = log.child({ component: "remembera" });
1414
2345
  }
2346
+ get authenticationConfigured() {
2347
+ return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
2348
+ }
1415
2349
  headers() {
1416
2350
  const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
1417
2351
  return key ? { authorization: `Bearer ${key}` } : {};
1418
2352
  }
1419
- async query(query) {
2353
+ /** Search delegates to the gateway's relevance query. */
2354
+ search(text, options = {}) {
2355
+ return defaultSearch(
2356
+ this,
2357
+ text,
2358
+ {
2359
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2360
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2361
+ }
2362
+ );
2363
+ }
2364
+ metadata() {
2365
+ return defaultMetadata(this.name, {
2366
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2367
+ relevanceRanking: true,
2368
+ backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
2369
+ });
2370
+ }
2371
+ /** @deprecated v1 alias of {@link retrieve}. */
2372
+ query(query) {
2373
+ return this.retrieve(query);
2374
+ }
2375
+ /** @deprecated v1 alias of {@link store}. */
2376
+ write(entry) {
2377
+ return this.store(entry);
2378
+ }
2379
+ async retrieve(query) {
1420
2380
  const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
1421
2381
  if (status !== 200) {
1422
2382
  throw new MemoryError(`Remembera query returned HTTP ${status}.`);
@@ -1427,7 +2387,7 @@ var RememberaMemoryProvider = class {
1427
2387
  }
1428
2388
  return entries.map((entry) => this.toEntry(entry)).filter((entry) => entry !== void 0);
1429
2389
  }
1430
- async write(entry) {
2390
+ async store(entry) {
1431
2391
  const { status } = await this.request(`${this.baseUrl}/api/memory`, entry);
1432
2392
  if (status !== 200 && status !== 201) {
1433
2393
  throw new MemoryError(`Remembera write returned HTTP ${status}.`);
@@ -1462,6 +2422,7 @@ var RememberaMemoryProvider = class {
1462
2422
  content: entry.content,
1463
2423
  scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
1464
2424
  ...Array.isArray(entry.tags) ? { tags: entry.tags.filter((t) => typeof t === "string") } : {},
2425
+ ...typeof entry.category === "string" ? { category: entry.category } : {},
1465
2426
  ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
1466
2427
  ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {}
1467
2428
  };
@@ -1492,57 +2453,54 @@ function createMemoryProvider(config) {
1492
2453
  }
1493
2454
 
1494
2455
  // src/memory/service.ts
2456
+ var DEFAULT_ENTRY_LIMIT = 8;
1495
2457
  var MAX_CONTEXT_CHARS = 4e3;
1496
2458
  var MemoryService = class {
1497
- constructor(provider, log = logger) {
2459
+ constructor(provider, log = logger, options = {}) {
1498
2460
  this.provider = provider;
1499
2461
  this.log = log.child({ component: "memory", provider: provider.name });
2462
+ this.required = options.required ?? false;
2463
+ this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
1500
2464
  }
1501
2465
  provider;
1502
2466
  log;
2467
+ required;
2468
+ maxContextChars;
1503
2469
  get providerRef() {
1504
2470
  return this.provider;
1505
2471
  }
1506
2472
  get providerName() {
1507
2473
  return this.provider.name;
1508
2474
  }
1509
- /** Relevant context for a task objective; empty string when nothing matches. */
1510
- async gatherContext(query) {
2475
+ get isRequired() {
2476
+ return this.required;
2477
+ }
2478
+ /** Structured, budgeted context for a task (§12 — avoid loading all memory). */
2479
+ async contextFor(query) {
2480
+ let entries;
1511
2481
  try {
1512
- const entries = await this.provider.query({
2482
+ entries = await this.provider.retrieve({
1513
2483
  text: query.objective,
1514
2484
  ...query.role ? { tags: [query.role] } : {},
1515
- limit: 8
2485
+ limit: query.limit ?? DEFAULT_ENTRY_LIMIT
1516
2486
  });
1517
- if (entries.length === 0) return "";
1518
- const byScope = /* @__PURE__ */ new Map();
1519
- for (const entry of entries) {
1520
- byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
1521
- }
1522
- const parts = [];
1523
- for (const scope of ["project", "task", "role", "knowledge"]) {
1524
- const scopeEntries = byScope.get(scope) ?? [];
1525
- if (scopeEntries.length === 0) continue;
1526
- const lines = scopeEntries.map((entry) => `- ${entry.content}`).join("\n");
1527
- parts.push(`### Relevant memory (${scope})
1528
- ${lines}`);
1529
- }
1530
- const context = parts.join("\n\n");
1531
- return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
1532
2487
  } catch (error) {
1533
- this.log.warn(
1534
- `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
1535
- );
1536
- return "";
2488
+ return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
1537
2489
  }
2490
+ return this.format(entries);
1538
2491
  }
1539
- /** Records a finished task's outcome — best-effort. */
2492
+ /** Prompt-ready context text (v1 convenience wrapper). */
2493
+ async gatherContext(query) {
2494
+ return (await this.contextFor(query)).text;
2495
+ }
2496
+ /** Records a finished task's outcome — task history plus "known problems" (§12). */
1540
2497
  async recordTaskOutcome(task) {
1541
2498
  const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
1542
2499
  try {
1543
- await this.provider.write({
2500
+ await this.provider.store({
1544
2501
  key: `task:${task.id}`,
1545
2502
  scope: "task",
2503
+ category: "tasks",
1546
2504
  content: `Objective: ${task.objective}
1547
2505
  Status: ${task.status}
1548
2506
  Outcome: ${outcome}`,
@@ -1553,18 +2511,53 @@ Outcome: ${outcome}`,
1553
2511
  (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
1554
2512
  );
1555
2513
  for (const { report, finding } of significant) {
1556
- await this.provider.write({
2514
+ await this.provider.store({
1557
2515
  key: `finding:${report.id}:${finding.id}`,
1558
2516
  scope: "project",
2517
+ category: "problems",
1559
2518
  content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
1560
2519
  tags: ["known-problem", report.agent]
1561
2520
  });
1562
2521
  }
1563
2522
  } catch (error) {
1564
- this.log.warn(
1565
- `Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
2523
+ this.handleFailure("store", error, void 0);
2524
+ }
2525
+ }
2526
+ format(entries) {
2527
+ if (entries.length === 0) return { text: "", entries, truncated: false };
2528
+ const byScope = /* @__PURE__ */ new Map();
2529
+ for (const entry of entries) {
2530
+ byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
2531
+ }
2532
+ const parts = [];
2533
+ for (const scope of ["project", "task", "role", "knowledge"]) {
2534
+ const scopeEntries = byScope.get(scope) ?? [];
2535
+ if (scopeEntries.length === 0) continue;
2536
+ const lines = scopeEntries.map((entry) => {
2537
+ const category = entry.category ? `[${entry.category}] ` : "";
2538
+ return `- ${category}${entry.content}`;
2539
+ }).join("\n");
2540
+ parts.push(`### Relevant memory (${scope})
2541
+ ${lines}`);
2542
+ }
2543
+ let text = parts.join("\n\n");
2544
+ const truncated = text.length > this.maxContextChars;
2545
+ if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
2546
+ return { text, entries, truncated };
2547
+ }
2548
+ /**
2549
+ * Failure policy (§12): continue unless memory is explicitly required.
2550
+ */
2551
+ handleFailure(operation, error, fallback) {
2552
+ const message = error instanceof Error ? error.message : String(error);
2553
+ if (this.required) {
2554
+ throw new MemoryError(
2555
+ `Memory is marked required, but ${operation} failed: ${message}`,
2556
+ { cause: error }
1566
2557
  );
1567
2558
  }
2559
+ this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
2560
+ return fallback ?? { text: "", entries: [], truncated: false };
1568
2561
  }
1569
2562
  };
1570
2563
 
@@ -1576,7 +2569,11 @@ function createTaskEngine(config, log = logger, directory) {
1576
2569
  agents,
1577
2570
  roles,
1578
2571
  store: new FileTaskStore(directory),
1579
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
2572
+ ...config.project.memory !== void 0 ? {
2573
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
2574
+ required: config.project.memory.required
2575
+ })
2576
+ } : {},
1580
2577
  audit: new FileAuditLog(),
1581
2578
  log
1582
2579
  });
@@ -1815,9 +2812,23 @@ var PipelineEngine = class {
1815
2812
  }
1816
2813
  options;
1817
2814
  log;
2815
+ /** Runs executing in THIS process — cancellation can terminate them directly. */
2816
+ activeRuns = /* @__PURE__ */ new Map();
1818
2817
  get store() {
1819
2818
  return this.options.store;
1820
2819
  }
2820
+ /**
2821
+ * Saves the run while preserving fields that may be set by another process
2822
+ * (e.g. a cancellation request) — an in-memory snapshot must never clobber
2823
+ * externally requested cancellation (§9.2).
2824
+ */
2825
+ async persist(run) {
2826
+ const current = await this.options.store.get(run.id);
2827
+ await this.options.store.save({
2828
+ ...run,
2829
+ ...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
2830
+ });
2831
+ }
1821
2832
  /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
1822
2833
  validate(def) {
1823
2834
  const ids = /* @__PURE__ */ new Set();
@@ -1917,18 +2928,44 @@ var PipelineEngine = class {
1917
2928
  );
1918
2929
  }
1919
2930
  }
2931
+ if (step.model) {
2932
+ const separator = step.model.indexOf(":");
2933
+ if (separator <= 0 || separator === step.model.length - 1) {
2934
+ throw new PipelineError(
2935
+ "PIPELINE_INVALID",
2936
+ `Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
2937
+ );
2938
+ }
2939
+ const providerName = step.model.slice(0, separator);
2940
+ if (!this.options.providers.some((p) => p.name === providerName)) {
2941
+ throw new PipelineError(
2942
+ "PIPELINE_INVALID",
2943
+ `Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
2944
+ );
2945
+ }
2946
+ }
1920
2947
  }
1921
2948
  const now = (/* @__PURE__ */ new Date()).toISOString();
1922
2949
  const run = {
1923
2950
  id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
1924
2951
  pipelineId: def.id,
2952
+ attempt: 1,
1925
2953
  objective,
1926
2954
  status: "running",
1927
2955
  startedAt: now,
1928
2956
  stepRuns: []
1929
2957
  };
1930
2958
  await this.options.store.save(run);
1931
- this.log.info(`Running pipeline ${def.id} as ${run.id}.`);
2959
+ const liveTaskIds = /* @__PURE__ */ new Set();
2960
+ this.activeRuns.set(run.id, {
2961
+ taskIds: liveTaskIds,
2962
+ cancelTasks: async () => {
2963
+ for (const taskId of liveTaskIds) {
2964
+ await this.options.tasks.cancel(taskId).catch(() => void 0);
2965
+ }
2966
+ }
2967
+ });
2968
+ this.log.info(`Running pipeline ${def.id} as ${run.id} (attempt ${run.attempt}).`);
1932
2969
  await this.options.audit?.record({
1933
2970
  at: (/* @__PURE__ */ new Date()).toISOString(),
1934
2971
  action: "pipeline.run.started",
@@ -1940,7 +2977,22 @@ var PipelineEngine = class {
1940
2977
  const pending = new Set(def.steps.map((s) => s.id));
1941
2978
  const contextParts = [];
1942
2979
  const allReports = [];
2980
+ let cancelled = false;
1943
2981
  while (pending.size > 0) {
2982
+ const persisted = await this.options.store.get(run.id);
2983
+ if (persisted?.cancellationRequested === true) {
2984
+ cancelled = true;
2985
+ run.cancellationRequested = true;
2986
+ run.cancellationRequestedAt = persisted.cancellationRequestedAt;
2987
+ await this.activeRuns.get(run.id)?.cancelTasks();
2988
+ for (const step of def.steps.filter((s) => pending.has(s.id))) {
2989
+ const stepRun = { stepId: step.id, status: "cancelled" };
2990
+ run.stepRuns.push(stepRun);
2991
+ options.onStep?.(stepRun);
2992
+ }
2993
+ pending.clear();
2994
+ break;
2995
+ }
1944
2996
  const ready = def.steps.filter(
1945
2997
  (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
1946
2998
  );
@@ -1957,7 +3009,8 @@ var PipelineEngine = class {
1957
3009
  ...role ? { role } : {},
1958
3010
  combinedContext: contextParts.join("\n\n"),
1959
3011
  reports: [...allReports],
1960
- pipelineRunId: run.id
3012
+ pipelineRunId: run.id,
3013
+ runId: run.id
1961
3014
  };
1962
3015
  return this.executeStep(step, ctx);
1963
3016
  })
@@ -1974,7 +3027,9 @@ ${contribution}`);
1974
3027
  }
1975
3028
  const stepRun = {
1976
3029
  stepId: step.id,
1977
- status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
3030
+ attempt: outcome.attempt ?? 1,
3031
+ ...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
3032
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
1978
3033
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
1979
3034
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
1980
3035
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
@@ -1983,10 +3038,12 @@ ${contribution}`);
1983
3038
  options.onStep?.(stepRun);
1984
3039
  });
1985
3040
  run.reports = [...allReports];
1986
- await this.options.store.save(run);
3041
+ await this.persist(run);
1987
3042
  }
1988
- const failed = [...outcomes.values()].some((o) => o.status === "failed");
1989
- const finalStatus = failed ? "failed" : "completed";
3043
+ const allOutcomes = [...outcomes.values()];
3044
+ const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
3045
+ const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
3046
+ const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
1990
3047
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
1991
3048
  let resultSummary;
1992
3049
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2014,16 +3071,19 @@ ${contribution}`);
2014
3071
  run.reports = collected;
2015
3072
  }
2016
3073
  }
3074
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3075
+ this.activeRuns.delete(run.id);
2017
3076
  const finished = {
2018
3077
  ...run,
2019
3078
  status: finalStatus,
2020
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3079
+ finishedAt,
3080
+ attempts: [...run.attempts ?? [], { attempt: run.attempt, startedAt: run.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
2021
3081
  ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
2022
3082
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
2023
3083
  ...planId !== void 0 ? { planId } : {},
2024
3084
  ...resultSummary !== void 0 ? { resultSummary } : {}
2025
3085
  };
2026
- await this.options.store.save(finished);
3086
+ await this.persist(finished);
2027
3087
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
2028
3088
  await this.options.audit?.record({
2029
3089
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2033,6 +3093,23 @@ ${contribution}`);
2033
3093
  });
2034
3094
  return finished;
2035
3095
  }
3096
+ /** First-class cancellation (§9.2): requests cancellation and terminates
3097
+ * tasks that are running in this process immediately. */
3098
+ async cancel(runId) {
3099
+ const run = await this.options.store.get(runId);
3100
+ if (!run) {
3101
+ throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
3102
+ }
3103
+ await this.options.store.save({
3104
+ ...run,
3105
+ cancellationRequested: true,
3106
+ cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
3107
+ });
3108
+ const active = this.activeRuns.get(runId);
3109
+ if (active) {
3110
+ await active.cancelTasks();
3111
+ }
3112
+ }
2036
3113
  async get(runId) {
2037
3114
  return this.options.store.get(runId);
2038
3115
  }
@@ -2056,9 +3133,16 @@ ${contribution}`);
2056
3133
  this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
2057
3134
  }
2058
3135
  last = await this.executeStepOnce(step, ctx);
2059
- if (last.status !== "failed") return last;
3136
+ if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
2060
3137
  }
2061
- return last;
3138
+ return { ...last, attempt: retries + 1 };
3139
+ }
3140
+ /** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
3141
+ trackLiveTasks(runId, taskIds) {
3142
+ if (!runId) return;
3143
+ const active = this.activeRuns.get(runId);
3144
+ if (!active) return;
3145
+ for (const id of taskIds) active.taskIds.add(id);
2062
3146
  }
2063
3147
  async executeStepOnce(step, ctx) {
2064
3148
  switch (step.kind) {
@@ -2091,7 +3175,10 @@ ${contribution}`);
2091
3175
  })
2092
3176
  )
2093
3177
  );
3178
+ this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
2094
3179
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
3180
+ const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
3181
+ const completedTasks = results.filter((r) => r.status === "completed");
2095
3182
  const failedTasks = results.filter((r) => r.status !== "completed");
2096
3183
  const lines = results.map((result, index) => {
2097
3184
  const agentName = agentNames[index] ?? "unknown";
@@ -2101,16 +3188,25 @@ ${contribution}`);
2101
3188
  const reports = results.flatMap((r) => r.result?.reports ?? []);
2102
3189
  const summary = lines.join("\n");
2103
3190
  if (failedTasks.length > 0) {
2104
- const firstError = failedTasks[0]?.result?.error ?? "unknown error";
3191
+ const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
3192
+ const states = failedTasks.map((r) => r.status).join(", ");
2105
3193
  return {
2106
- status: "failed",
3194
+ // Some agents succeeded → the step produced usable but incomplete
3195
+ // intelligence: partial, not failed (roadmap §9.3).
3196
+ status: completedTasks.length > 0 ? "partial" : "failed",
2107
3197
  summary,
2108
3198
  taskIds: created.map((t) => t.id),
2109
- error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
3199
+ error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
2110
3200
  reports
2111
3201
  };
2112
3202
  }
2113
- return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
3203
+ return {
3204
+ status: "completed",
3205
+ summary,
3206
+ taskIds: created.map((t) => t.id),
3207
+ ...executionIds.length > 0 ? { executionIds } : {},
3208
+ reports
3209
+ };
2114
3210
  }
2115
3211
  async runSingle(step, ctx, agentName) {
2116
3212
  const task = await this.options.tasks.create({
@@ -2119,10 +3215,13 @@ ${contribution}`);
2119
3215
  ...ctx.role ? { role: ctx.role.id } : {},
2120
3216
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2121
3217
  });
3218
+ this.trackLiveTasks(ctx.runId, [task.id]);
2122
3219
  const result = await this.options.tasks.run(task.id);
2123
3220
  if (result.status !== "completed") {
2124
3221
  return {
2125
- status: "failed",
3222
+ // Unknown/timed_out outcomes are not failures — the work may have
3223
+ // happened remotely; the step is incomplete, not broken.
3224
+ status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
2126
3225
  summary: result.result?.summary ?? "",
2127
3226
  taskIds: [task.id],
2128
3227
  error: result.result?.error ?? `task ${task.id} did not complete`,
@@ -2302,6 +3401,94 @@ function buildPlanner(plannerConfig, chat, log = logger) {
2302
3401
 
2303
3402
  // src/reports/aggregate.ts
2304
3403
  import { randomBytes as randomBytes5 } from "crypto";
3404
+
3405
+ // src/reports/schema.ts
3406
+ import { z as z3 } from "zod";
3407
+ var evidenceSchema = z3.union([
3408
+ z3.string().transform((excerpt) => ({ kind: "observation", excerpt })),
3409
+ z3.object({
3410
+ kind: z3.enum(["file", "line", "url", "command", "observation", "artifact"]),
3411
+ ref: z3.string().optional(),
3412
+ line: z3.number().int().optional(),
3413
+ excerpt: z3.string().optional(),
3414
+ agent: z3.string().optional()
3415
+ })
3416
+ ]);
3417
+ var findingSchema = z3.object({
3418
+ id: z3.string().min(1),
3419
+ title: z3.string().min(1),
3420
+ detail: z3.string().default(""),
3421
+ description: z3.string().optional(),
3422
+ severity: z3.enum(["info", "low", "medium", "high", "critical"]).optional(),
3423
+ confidence: z3.enum(["low", "medium", "high"]).optional(),
3424
+ severityBySource: z3.record(z3.enum(["info", "low", "medium", "high", "critical"])).optional(),
3425
+ confidenceBySource: z3.record(z3.enum(["low", "medium", "high"])).optional(),
3426
+ recommendations: z3.array(z3.string()).optional(),
3427
+ evidence: z3.array(evidenceSchema).optional(),
3428
+ location: z3.string().optional(),
3429
+ tags: z3.array(z3.string()).optional(),
3430
+ source: z3.object({ agent: z3.string().optional(), reportId: z3.string().optional(), pipelineRunId: z3.string().optional() }).optional(),
3431
+ recommendation: z3.string().optional()
3432
+ });
3433
+ var artifactSchema = z3.object({
3434
+ kind: z3.enum(["file", "patch", "log", "screenshot", "document", "data"]),
3435
+ ref: z3.string().min(1),
3436
+ description: z3.string().optional(),
3437
+ mediaType: z3.string().optional(),
3438
+ bytes: z3.number().int().optional()
3439
+ });
3440
+ var reportSchema = z3.object({
3441
+ id: z3.string().min(1),
3442
+ taskId: z3.string().min(1),
3443
+ agent: z3.string().min(1),
3444
+ agentId: z3.string().optional(),
3445
+ pipelineRunId: z3.string().optional(),
3446
+ model: z3.string().optional(),
3447
+ summary: z3.string(),
3448
+ findings: z3.array(findingSchema),
3449
+ recommendations: z3.array(z3.string()),
3450
+ confidence: z3.enum(["low", "medium", "high"]).optional(),
3451
+ evidence: z3.array(evidenceSchema).optional(),
3452
+ artifacts: z3.array(artifactSchema).optional(),
3453
+ createdAt: z3.string().min(1),
3454
+ metadata: z3.record(z3.unknown()).optional()
3455
+ });
3456
+ function validateReport(value) {
3457
+ const parsed = reportSchema.safeParse(value);
3458
+ if (!parsed.success) {
3459
+ const issues = parsed.error.issues.map((issue) => ({
3460
+ path: issue.path.join(".") || "(root)",
3461
+ message: issue.message
3462
+ }));
3463
+ return {
3464
+ valid: false,
3465
+ issues,
3466
+ reason: `report failed schema validation (${issues.length} issue(s))`
3467
+ };
3468
+ }
3469
+ const report = parsed.data;
3470
+ report.findings = report.findings.map((finding) => ({
3471
+ ...finding,
3472
+ description: finding.description ?? finding.detail,
3473
+ ...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
3474
+ }));
3475
+ return { valid: true, report };
3476
+ }
3477
+ function normalizeEvidence(input) {
3478
+ if (input === void 0) return void 0;
3479
+ return input.map(
3480
+ (item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
3481
+ );
3482
+ }
3483
+ function normalizeFinding(finding) {
3484
+ return {
3485
+ ...finding,
3486
+ description: finding.description ?? finding.detail,
3487
+ evidence: normalizeEvidence(finding.evidence)
3488
+ };
3489
+ }
3490
+
3491
+ // src/reports/aggregate.ts
2305
3492
  var SEVERITY_RANK2 = {
2306
3493
  info: 0,
2307
3494
  low: 1,
@@ -2323,7 +3510,12 @@ function aggregateReports(reports, options = {}) {
2323
3510
  }
2324
3511
  const sources = [...new Set(reports.map((r) => r.agent))].sort();
2325
3512
  const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
2326
- const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${recommendationGroups.length} recommendation group(s).`;
3513
+ const conflicts = detectConflicts(findingGroups);
3514
+ const agreements = findingGroups.filter(
3515
+ (group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
3516
+ );
3517
+ const disputed = conflicts.length;
3518
+ const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${agreements.length} agreement(s), ${disputed} conflict(s), ${recommendationGroups.length} recommendation group(s).`;
2327
3519
  return {
2328
3520
  id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2329
3521
  title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
@@ -2332,10 +3524,57 @@ function aggregateReports(reports, options = {}) {
2332
3524
  taskIds: options.taskIds ?? [],
2333
3525
  summary,
2334
3526
  findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
3527
+ agreements,
3528
+ conflicts,
2335
3529
  recommendations: recommendationGroups,
2336
3530
  sources,
2337
3531
  reportIds: reports.map((r) => r.id),
2338
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
3532
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3533
+ metadata: {
3534
+ agents: sources.length,
3535
+ reports: reports.length,
3536
+ rawFindings: totalRawFindings,
3537
+ findingGroups: findingGroups.length,
3538
+ agreements: agreements.length,
3539
+ conflicts: conflicts.length
3540
+ }
3541
+ };
3542
+ }
3543
+ function rankConfidence(confidence) {
3544
+ return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
3545
+ }
3546
+ function detectConflicts(groups) {
3547
+ const conflicts = [];
3548
+ for (const group of groups) {
3549
+ if (group.sources.length < 2) continue;
3550
+ const severities = new Set(Object.values(group.severityBySource ?? {}));
3551
+ if (severities.size > 1) {
3552
+ conflicts.push({
3553
+ type: "severity",
3554
+ findingTitle: group.title,
3555
+ positions: group.sources.map((source) => position(source, group)),
3556
+ note: `Agents disagree on severity (${[...severities].sort((a, b) => SEVERITY_RANK2[a] - SEVERITY_RANK2[b]).join(", ")}); the highest severity is shown, every position is preserved.`
3557
+ });
3558
+ }
3559
+ const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
3560
+ if (recommendations.length > 1) {
3561
+ conflicts.push({
3562
+ type: "recommendation",
3563
+ findingTitle: group.title,
3564
+ positions: group.sources.map((source) => position(source, group)),
3565
+ note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
3566
+ });
3567
+ }
3568
+ }
3569
+ return conflicts;
3570
+ }
3571
+ function position(source, group) {
3572
+ return {
3573
+ agent: source.agent,
3574
+ reportId: source.reportId,
3575
+ ...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
3576
+ ...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
3577
+ ...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
2339
3578
  };
2340
3579
  }
2341
3580
  function rank(finding) {
@@ -2348,10 +3587,22 @@ function mergeFinding(groups, finding, report, threshold) {
2348
3587
  group.occurrences += 1;
2349
3588
  group.details.push(finding.detail);
2350
3589
  group.sources.push(source);
2351
- group.evidence.push(...finding.evidence ?? []);
3590
+ group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
2352
3591
  if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
2353
3592
  group.severity = finding.severity;
2354
3593
  }
3594
+ if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
3595
+ group.confidence = finding.confidence;
3596
+ }
3597
+ if (finding.severity) {
3598
+ group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
3599
+ }
3600
+ if (finding.confidence) {
3601
+ group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
3602
+ }
3603
+ if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
3604
+ group.recommendations = [...group.recommendations ?? [], finding.recommendation];
3605
+ }
2355
3606
  return;
2356
3607
  }
2357
3608
  groups.push({
@@ -2360,7 +3611,11 @@ function mergeFinding(groups, finding, report, threshold) {
2360
3611
  sources: [source],
2361
3612
  occurrences: 1,
2362
3613
  ...finding.severity ? { severity: finding.severity } : {},
2363
- evidence: [...finding.evidence ?? []]
3614
+ ...finding.confidence ? { confidence: finding.confidence } : {},
3615
+ ...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
3616
+ ...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
3617
+ ...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
3618
+ evidence: [...normalizeEvidence(finding.evidence) ?? []]
2364
3619
  });
2365
3620
  }
2366
3621
  function mergeRecommendation(groups, text, report, threshold) {
@@ -2442,6 +3697,7 @@ function extractReportFromText(raw, meta) {
2442
3697
  id: `${meta.taskId}-f-${index + 1}`,
2443
3698
  title: truncate3(text, 120),
2444
3699
  detail: text,
3700
+ description: text,
2445
3701
  severity: guessSeverity(text)
2446
3702
  }));
2447
3703
  return {
@@ -2452,7 +3708,8 @@ function extractReportFromText(raw, meta) {
2452
3708
  summary,
2453
3709
  findings,
2454
3710
  recommendations: recommendationTexts.map((t) => truncate3(t, 200)),
2455
- metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
3711
+ ...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
3712
+ metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
2456
3713
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2457
3714
  };
2458
3715
  }
@@ -2468,27 +3725,46 @@ var ReportService = class {
2468
3725
  }
2469
3726
  store;
2470
3727
  log;
3728
+ /** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
3729
+ lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
2471
3730
  get storeRef() {
2472
3731
  return this.store;
2473
3732
  }
2474
- /** Normalizes finished tasks into reports (§17 — normalizer). */
3733
+ /** Normalizes finished tasks into reports (§17 — normalizer), validating each
3734
+ * one so malformed reports never reach aggregation (§13). */
2475
3735
  async collectFromTasks(tasks) {
2476
- const reports = [];
3736
+ const candidates = [];
2477
3737
  for (const task of tasks) {
2478
3738
  const structured = task.result?.reports ?? [];
2479
3739
  if (structured.length > 0) {
2480
- reports.push(...structured);
3740
+ candidates.push(...structured);
2481
3741
  continue;
2482
3742
  }
2483
3743
  const text = task.result?.rawOutput ?? task.result?.summary ?? "";
2484
3744
  if (text.trim().length === 0) continue;
2485
- reports.push(
3745
+ candidates.push(
2486
3746
  extractReportFromText(text, {
2487
3747
  taskId: task.id,
2488
3748
  agent: task.agent ?? "unknown"
2489
3749
  })
2490
3750
  );
2491
3751
  }
3752
+ const reports = [];
3753
+ const rejected = [];
3754
+ for (const candidate of candidates) {
3755
+ const result = validateReport(candidate);
3756
+ if (result.valid) {
3757
+ reports.push(result.report);
3758
+ } else {
3759
+ rejected.push({
3760
+ key: candidate?.id ?? "(unknown)",
3761
+ reason: result.reason,
3762
+ issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
3763
+ });
3764
+ this.log.warn(`Rejected malformed report: ${result.reason}`);
3765
+ }
3766
+ }
3767
+ this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
2492
3768
  return reports;
2493
3769
  }
2494
3770
  async saveReports(reports) {
@@ -2556,7 +3832,11 @@ function buildPipelineEngine(config, log = logger) {
2556
3832
  agents,
2557
3833
  roles,
2558
3834
  store: new FileTaskStore(),
2559
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
3835
+ ...config.project.memory !== void 0 ? {
3836
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
3837
+ required: config.project.memory.required
3838
+ })
3839
+ } : {},
2560
3840
  audit: new FileAuditLog(),
2561
3841
  log
2562
3842
  });
@@ -2594,12 +3874,14 @@ var ProjectStores = class {
2594
3874
  };
2595
3875
 
2596
3876
  // src/version.ts
2597
- var OMNINODE_VERSION = "2.0.0-alpha.1";
3877
+ var OMNINODE_VERSION = "2.0.0-alpha.10";
2598
3878
  export {
3879
+ AgentAdapterRegistry,
2599
3880
  AgentError,
2600
3881
  AgentRegistry,
2601
3882
  ConfigError,
2602
3883
  ConsoleLogSink,
3884
+ DEFAULT_MAX_OUTPUT_BYTES,
2603
3885
  FileAuditLog,
2604
3886
  FilePipelineRunStore,
2605
3887
  FilePlanStore,
@@ -2609,6 +3891,8 @@ export {
2609
3891
  JsonFileStore,
2610
3892
  LocalMemoryProvider,
2611
3893
  Logger,
3894
+ MAX_CONTEXT_CHARS,
3895
+ MAX_MESSAGE_BYTES,
2612
3896
  MemoryError,
2613
3897
  MemoryService,
2614
3898
  ModelPlanner,
@@ -2618,16 +3902,22 @@ export {
2618
3902
  OmniHilbrasProvider,
2619
3903
  OmniNodeError,
2620
3904
  OpenAICompatibleProvider,
3905
+ PROTOCOL_V2,
2621
3906
  PipelineEngine,
2622
3907
  PipelineError,
2623
3908
  PlannerError,
2624
3909
  ProcessAgent,
3910
+ ProcessAgentAdapter,
2625
3911
  ProjectStores,
3912
+ ProtocolDecoder,
2626
3913
  ProtocolError,
3914
+ ProtocolSession,
2627
3915
  ProviderError,
2628
3916
  RememberaMemoryProvider,
2629
3917
  ReportService,
2630
3918
  RoleRegistry,
3919
+ SUPPORTED_PROTOCOL_VERSIONS,
3920
+ TERMINAL_TASK_STATUSES,
2631
3921
  TaskEngine,
2632
3922
  TaskError,
2633
3923
  agentConfigSchema,
@@ -2636,6 +3926,8 @@ export {
2636
3926
  authHeaders,
2637
3927
  buildAgentRegistry,
2638
3928
  buildChildEnv,
3929
+ buildHello,
3930
+ buildHelloAck,
2639
3931
  buildPipelineEngine,
2640
3932
  buildPlanner,
2641
3933
  buildPlannerContext,
@@ -2646,24 +3938,52 @@ export {
2646
3938
  createMemoryProvider,
2647
3939
  createProvider,
2648
3940
  createTaskEngine,
3941
+ decodeMessage,
3942
+ decodeStream,
3943
+ defaultMetadata,
2649
3944
  defaultProjectConfigYaml,
3945
+ defaultSearch,
3946
+ describeExit,
3947
+ detectConflicts,
3948
+ encodeMessage,
3949
+ envelope,
2650
3950
  expandEnvRefs,
3951
+ extractMessage,
2651
3952
  extractReportFromText,
2652
3953
  findConfigFile,
3954
+ isNetworkError,
2653
3955
  isOmniNodeError,
3956
+ isRetryable,
3957
+ isSupportedProtocol,
3958
+ isTimeoutError,
2654
3959
  keywords,
3960
+ killTree,
3961
+ kindFromStatus,
2655
3962
  loadConfig,
2656
3963
  logger,
3964
+ newMessageId,
3965
+ normalizeEvidence,
3966
+ normalizeFinding,
3967
+ parseHello,
2657
3968
  parsePlanJson,
3969
+ parseRetryAfter,
2658
3970
  pipelineConfigSchema,
2659
3971
  pipelineStepConfigSchema,
2660
3972
  plannerConfigSchema,
2661
3973
  projectConfigSchema,
2662
3974
  providerConfigSchema,
3975
+ providerHttpError,
3976
+ providerTransportError,
2663
3977
  registryKey,
2664
3978
  requestJson,
3979
+ requireCompletion,
2665
3980
  resolveApiKey,
3981
+ resolveEnvPolicy,
2666
3982
  roleConfigSchema,
2667
- similar
3983
+ sanitizeChunk,
3984
+ similar,
3985
+ validateEnvelope,
3986
+ validateReport,
3987
+ validateWorkingDirectory
2668
3988
  };
2669
3989
  //# sourceMappingURL=index.js.map