@purista/harness 1.2.2 → 1.2.4

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.
@@ -552,7 +552,15 @@ export interface RunSummary {
552
552
  agentCalls: number;
553
553
  error?: SerializedError;
554
554
  }
555
- /** Harness streaming events emitted from `session.workflows.<id>.stream(...)`. */
555
+ /**
556
+ * Harness streaming events emitted from `session.workflows.<id>.stream(...)`.
557
+ *
558
+ * `text(...)` and `object(...)` model calls return final results and do not
559
+ * expose partial output. Consumed model streams are private by default.
560
+ * `model.delta`, `model.object.partial`, and streamed `model.object` are
561
+ * emitted only when that `textStream(...)` or `objectStream(...)` call passes
562
+ * `{ emitRunEvents: true }`.
563
+ */
556
564
  export type RunEvent = {
557
565
  type: 'run.started';
558
566
  runId: string;
@@ -578,7 +586,10 @@ export type RunEvent = {
578
586
  } | {
579
587
  type: 'model.delta';
580
588
  runId: string;
581
- agentId: string;
589
+ streamId: string;
590
+ agentId?: string;
591
+ workflowId?: string;
592
+ modelAlias?: string;
582
593
  delta: string;
583
594
  } | {
584
595
  type: 'tool.started';
@@ -603,12 +614,18 @@ export type RunEvent = {
603
614
  } | {
604
615
  type: 'model.object.partial';
605
616
  runId: string;
617
+ streamId: string;
606
618
  agentId?: string;
619
+ workflowId?: string;
620
+ modelAlias?: string;
607
621
  partial: JsonValue;
608
622
  } | {
609
623
  type: 'model.object';
610
624
  runId: string;
611
625
  agentId?: string;
626
+ workflowId?: string;
627
+ modelAlias?: string;
628
+ streamId?: string;
612
629
  object: JsonValue;
613
630
  usage?: TokenUsage;
614
631
  } | {
@@ -2,11 +2,18 @@ import type { EmbeddingRequest, EmbeddingResponse, ContentPart, ModelAlias, Mode
2
2
  import type { TelemetryShim } from '../telemetry/index.js';
3
3
  import type { JsonValue } from './json.js';
4
4
  export interface ModelInvokeContext {
5
- harnessName: string;
6
- sessionId: string;
7
- runId: string;
5
+ /** Harness instance name used for telemetry and run-event attribution. */
6
+ harnessName?: string;
7
+ /** Session id used for telemetry and run-event attribution. */
8
+ sessionId?: string;
9
+ /** Run id used for telemetry and run-event attribution. */
10
+ runId?: string;
11
+ /** Workflow id when the model call belongs to a workflow run. */
8
12
  workflowId?: string;
13
+ /** Agent id when the model call belongs to an agent run. */
9
14
  agentId?: string;
15
+ /** Mirrors consumed stream chunks into the enclosing session `RunEvent` stream. Defaults to false. */
16
+ emitRunEvents?: boolean;
10
17
  }
11
18
  type TextPart = Extract<ContentPart, {
12
19
  kind: 'text';
@@ -45,6 +45,7 @@ export declare abstract class BaseModelProvider implements ModelProvider {
45
45
  protected doObjectStream<T extends JsonValue = JsonValue>(_req: ObjectRequest<T>): AsyncIterable<ObjectStreamChunk<T>>;
46
46
  protected doEmbed(_req: EmbeddingRequest): Promise<EmbeddingResponse>;
47
47
  protected doRerank(_req: RerankRequest): Promise<RerankResponse>;
48
+ protected getLogger(): Logger | undefined;
48
49
  protected normalizeError(error: unknown, method: ProviderMethod, req: ProviderRequest): HarnessError;
49
50
  private call;
50
51
  private stream;
@@ -67,6 +67,9 @@ export class BaseModelProvider {
67
67
  doRerank(_req) {
68
68
  throw this.methodMissing('rerank');
69
69
  }
70
+ getLogger() {
71
+ return this.logger;
72
+ }
70
73
  normalizeError(error, method, req) {
71
74
  if (error instanceof HarnessError)
72
75
  return error;
@@ -433,7 +433,12 @@ export function createSessionHarness(definition) {
433
433
  input,
434
434
  history: await definition.state.listMessages(sessionId),
435
435
  agent,
436
- models: modelRegistry,
436
+ models: withRunEventModelRegistry(modelRegistry, {
437
+ harnessName: definition.name,
438
+ sessionId,
439
+ runId,
440
+ agentId
441
+ }, emit),
437
442
  skills: resolvedSkills,
438
443
  customTools: definition.tools,
439
444
  mcpRegistry,
@@ -585,7 +590,12 @@ export function createSessionHarness(definition) {
585
590
  signal: runSignal.signal,
586
591
  runId,
587
592
  sessionId,
588
- models: modelRegistry,
593
+ models: withRunEventModelRegistry(modelRegistry, {
594
+ harnessName: definition.name,
595
+ sessionId,
596
+ runId,
597
+ workflowId
598
+ }, emit),
589
599
  metadata: opts?.metadata ?? {},
590
600
  metrics: workflowMetrics,
591
601
  memory,
@@ -615,7 +625,13 @@ export function createSessionHarness(definition) {
615
625
  input: agentInput,
616
626
  history: await definition.state.listMessages(sessionId),
617
627
  agent: agent,
618
- models: modelRegistry,
628
+ models: withRunEventModelRegistry(modelRegistry, {
629
+ harnessName: definition.name,
630
+ sessionId,
631
+ runId,
632
+ workflowId,
633
+ agentId
634
+ }, emit),
619
635
  skills: resolvedSkills,
620
636
  customTools: definition.tools,
621
637
  mcpRegistry,
@@ -764,6 +780,100 @@ export function createSessionHarness(definition) {
764
780
  }
765
781
  }
766
782
  }
783
+ function withRunEventModelRegistry(models, context, emitEvent) {
784
+ return Object.fromEntries(Object.entries(models).map(([alias, handle]) => [alias, withRunEventModelHandle(alias, handle, context, emitEvent)]));
785
+ }
786
+ function withRunEventModelHandle(alias, handle, context, emitEvent) {
787
+ if (!handle || typeof handle !== 'object')
788
+ return handle;
789
+ const source = handle;
790
+ const wrapped = { ...source };
791
+ for (const method of ['text', 'object', 'embed', 'rerank']) {
792
+ const fn = source[method];
793
+ if (typeof fn !== 'function')
794
+ continue;
795
+ wrapped[method] = (req, signal, ctx) => fn.call(source, req, signal, mergeModelRunContext(context, ctx));
796
+ }
797
+ const textStream = source['textStream'];
798
+ if (typeof textStream === 'function') {
799
+ wrapped['textStream'] = (req, signal, ctx) => {
800
+ const streamContext = modelStreamRunContext(context, ctx, alias);
801
+ return emitTextStreamRunEvents(textStream.call(source, req, signal, streamContext), streamContext, emitEvent);
802
+ };
803
+ }
804
+ const objectStream = source['objectStream'];
805
+ if (typeof objectStream === 'function') {
806
+ wrapped['objectStream'] = (req, signal, ctx) => {
807
+ const streamContext = modelStreamRunContext(context, ctx, alias);
808
+ return emitObjectStreamRunEvents(objectStream.call(source, req, signal, streamContext), streamContext, emitEvent);
809
+ };
810
+ }
811
+ return wrapped;
812
+ }
813
+ function mergeModelRunContext(context, override) {
814
+ return { ...context, ...(override ?? {}) };
815
+ }
816
+ function modelStreamRunContext(context, override, alias) {
817
+ const merged = mergeModelRunContext(context, override);
818
+ return {
819
+ ...merged,
820
+ modelAlias: alias,
821
+ ...(merged.emitRunEvents === true ? { streamId: `model_${ulid()}` } : {})
822
+ };
823
+ }
824
+ async function* emitTextStreamRunEvents(stream, context, emitEvent) {
825
+ for await (const chunk of stream) {
826
+ if (context.emitRunEvents === true && isTextDeltaChunk(chunk)) {
827
+ await emitEvent({
828
+ type: 'model.delta',
829
+ runId: context.runId,
830
+ ...(context.agentId ? { agentId: context.agentId } : {}),
831
+ ...(context.workflowId ? { workflowId: context.workflowId } : {}),
832
+ ...(context.modelAlias ? { modelAlias: context.modelAlias } : {}),
833
+ streamId: context.streamId,
834
+ delta: chunk.text
835
+ });
836
+ }
837
+ yield chunk;
838
+ }
839
+ }
840
+ async function* emitObjectStreamRunEvents(stream, context, emitEvent) {
841
+ for await (const chunk of stream) {
842
+ if (context.emitRunEvents === true && isObjectPartialChunk(chunk)) {
843
+ await emitEvent({
844
+ type: 'model.object.partial',
845
+ runId: context.runId,
846
+ ...(context.agentId ? { agentId: context.agentId } : {}),
847
+ ...(context.workflowId ? { workflowId: context.workflowId } : {}),
848
+ ...(context.modelAlias ? { modelAlias: context.modelAlias } : {}),
849
+ streamId: context.streamId,
850
+ partial: chunk.partial
851
+ });
852
+ }
853
+ else if (context.emitRunEvents === true && isObjectFinishChunk(chunk)) {
854
+ await emitEvent({
855
+ type: 'model.object',
856
+ runId: context.runId,
857
+ ...(context.agentId ? { agentId: context.agentId } : {}),
858
+ ...(context.workflowId ? { workflowId: context.workflowId } : {}),
859
+ ...(context.modelAlias ? { modelAlias: context.modelAlias } : {}),
860
+ ...(context.streamId ? { streamId: context.streamId } : {}),
861
+ object: chunk.object,
862
+ ...(chunk.usage ? { usage: chunk.usage } : {})
863
+ });
864
+ }
865
+ yield chunk;
866
+ }
867
+ }
868
+ function isTextDeltaChunk(chunk) {
869
+ return Boolean(chunk && typeof chunk === 'object' && chunk.kind === 'delta' && typeof chunk.text === 'string');
870
+ }
871
+ function isObjectPartialChunk(chunk) {
872
+ return Boolean(chunk && typeof chunk === 'object' && chunk.kind === 'partial');
873
+ }
874
+ function isObjectFinishChunk(chunk) {
875
+ return Boolean(chunk && typeof chunk === 'object' && chunk.kind === 'finish' && Object.prototype.hasOwnProperty.call(chunk, 'object'));
876
+ }
767
877
  function configureHarnessAdapters(context, models, state, sandbox, memory, tools) {
768
878
  const seen = new Set();
769
879
  for (const alias of Object.values(models)) {
@@ -940,12 +1050,12 @@ function sanitizeEventForPersistence(event) {
940
1050
  case 'model.message':
941
1051
  return { agentId: event.agentId, message: '[redacted]' };
942
1052
  case 'model.delta':
943
- return { agentId: event.agentId, delta: '[redacted]' };
1053
+ return { ...modelStreamEventMeta(event), delta: '[redacted]' };
944
1054
  case 'model.object.partial':
945
- return { ...(event.agentId ? { agentId: event.agentId } : {}), partial: '[redacted]' };
1055
+ return { ...modelStreamEventMeta(event), partial: '[redacted]' };
946
1056
  case 'model.object':
947
1057
  return {
948
- ...(event.agentId ? { agentId: event.agentId } : {}),
1058
+ ...modelStreamEventMeta(event),
949
1059
  object: '[redacted]',
950
1060
  ...(event.usage ? { usage: event.usage } : {})
951
1061
  };
@@ -973,6 +1083,14 @@ function sanitizeEventForPersistence(event) {
973
1083
  }
974
1084
  }
975
1085
  }
1086
+ function modelStreamEventMeta(event) {
1087
+ return {
1088
+ ...(event.agentId ? { agentId: event.agentId } : {}),
1089
+ ...(event.workflowId ? { workflowId: event.workflowId } : {}),
1090
+ ...(event.modelAlias ? { modelAlias: event.modelAlias } : {}),
1091
+ ...(event.streamId ? { streamId: event.streamId } : {})
1092
+ };
1093
+ }
976
1094
  function isJsonRecord(value) {
977
1095
  return value !== null && typeof value === 'object' && !Array.isArray(value);
978
1096
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purista/harness",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Self-hosted enterprise agent harness for typed tools, agents, workflows, state, sandboxing, and telemetry.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",