@introspection-sdk/introspection-node 0.4.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/index.d.ts +4 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +4 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/resources/experiments.d.ts +7 -3
  7. package/dist/resources/experiments.d.ts.map +1 -1
  8. package/dist/resources/experiments.js +11 -10
  9. package/dist/resources/experiments.js.map +1 -1
  10. package/dist/resources/recipes.d.ts +7 -4
  11. package/dist/resources/recipes.d.ts.map +1 -1
  12. package/dist/resources/recipes.js +11 -10
  13. package/dist/resources/recipes.js.map +1 -1
  14. package/dist/resources/runtimes.d.ts +7 -3
  15. package/dist/resources/runtimes.d.ts.map +1 -1
  16. package/dist/resources/runtimes.js +23 -19
  17. package/dist/resources/runtimes.js.map +1 -1
  18. package/dist/runner-resources/conversations.d.ts +69 -0
  19. package/dist/runner-resources/conversations.d.ts.map +1 -0
  20. package/dist/runner-resources/conversations.js +167 -0
  21. package/dist/runner-resources/conversations.js.map +1 -0
  22. package/dist/runner-resources/files.d.ts +12 -5
  23. package/dist/runner-resources/files.d.ts.map +1 -1
  24. package/dist/runner-resources/files.js +21 -18
  25. package/dist/runner-resources/files.js.map +1 -1
  26. package/dist/runner-resources/tasks.d.ts +7 -3
  27. package/dist/runner-resources/tasks.d.ts.map +1 -1
  28. package/dist/runner-resources/tasks.js +11 -9
  29. package/dist/runner-resources/tasks.js.map +1 -1
  30. package/dist/runner.d.ts +5 -2
  31. package/dist/runner.d.ts.map +1 -1
  32. package/dist/runner.js +6 -2
  33. package/dist/runner.js.map +1 -1
  34. package/dist/utils.d.ts +12 -6
  35. package/dist/utils.d.ts.map +1 -1
  36. package/dist/utils.js +11 -7
  37. package/dist/utils.js.map +1 -1
  38. package/package.json +7 -6
  39. package/src/index.ts +23 -1
  40. package/src/proxy-from-env.d.ts +7 -0
  41. package/src/resources/experiments.ts +11 -11
  42. package/src/resources/recipes.ts +11 -11
  43. package/src/resources/runtimes.ts +22 -20
  44. package/src/runner-resources/conversations.ts +198 -0
  45. package/src/runner-resources/files.ts +21 -20
  46. package/src/runner-resources/tasks.ts +11 -10
  47. package/src/runner.ts +6 -2
  48. package/src/utils.ts +12 -8
@@ -0,0 +1,198 @@
1
+ import type {
2
+ ConversationItem,
3
+ ConversationItemInclude,
4
+ ConversationItemList,
5
+ ConversationItemListParams,
6
+ ConversationListParams,
7
+ ConversationResponse,
8
+ ConversationSummary,
9
+ MessagePart,
10
+ Paginated,
11
+ ToolCallResponsePart,
12
+ } from "@introspection-sdk/types";
13
+ import { HttpClient } from "../http.js";
14
+
15
+ /** Includes requested when building a {@link ConversationResponse}. */
16
+ const RESPONSE_INCLUDES: ConversationItemInclude[] = [
17
+ "gen_ai.input.messages",
18
+ "gen_ai.system_instructions",
19
+ "gen_ai.tool.definitions",
20
+ ];
21
+
22
+ /**
23
+ * Items of a conversation (`/v1/conversations/{id}/items`). Read-only.
24
+ *
25
+ * Paging is OpenAI-style underneath: the envelope has NO `next` token —
26
+ * `list()` drives `after` = the previous page's `last_id` while
27
+ * `has_more` is true.
28
+ */
29
+ export class ConversationItemsApi {
30
+ constructor(private readonly http: HttpClient) {}
31
+
32
+ /**
33
+ * Iterate items of a conversation. Pages are fetched lazily as the
34
+ * iterator is consumed (`limit` sets the page size, `after` the
35
+ * starting cursor); stop early to stop fetching. Pass
36
+ * `order: "asc"` to walk the transcript from the start.
37
+ *
38
+ * Items carry the turn-local delta in `input_messages` — only the
39
+ * messages new to that turn. Use `get()` for the full input history.
40
+ */
41
+ async *list(
42
+ conversationId: string,
43
+ params?: ConversationItemListParams,
44
+ ): AsyncIterable<ConversationItem> {
45
+ let after: string | undefined = params?.after;
46
+ for (;;) {
47
+ const page = await this.http.request<ConversationItemList>({
48
+ method: "GET",
49
+ path: `/v1/conversations/${encodeURIComponent(conversationId)}/items`,
50
+ query: { ...params, after } as Record<string, unknown>,
51
+ });
52
+ for (const item of page.data) yield item;
53
+ if (!page.has_more || page.last_id === null) return;
54
+ after = page.last_id;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Fetch a single conversation item. Unlike the list route, the
60
+ * detail's `input_messages` is the FULL input history for that span.
61
+ */
62
+ get(
63
+ conversationId: string,
64
+ itemId: string,
65
+ params?: { include?: ConversationItemInclude[] },
66
+ ): Promise<ConversationItem> {
67
+ return this.http.request<ConversationItem>({
68
+ method: "GET",
69
+ path: `/v1/conversations/${encodeURIComponent(conversationId)}/items/${encodeURIComponent(itemId)}`,
70
+ query: params as Record<string, unknown> | undefined,
71
+ });
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Read-only Conversations API (`/v1/conversations`).
77
+ *
78
+ * Both `list()` and `items.list()` are auto-paging async generators,
79
+ * but they drive different wire protocols underneath: `list()` walks
80
+ * the standard Introspection cursor envelope's opaque `next` token,
81
+ * while `items.list()` walks an OpenAI-style envelope via `after` =
82
+ * the previous page's `last_id` while `has_more` is true.
83
+ */
84
+ export class ConversationsApi {
85
+ /** Items of a conversation — `conversations.items.list(...)` etc. */
86
+ readonly items: ConversationItemsApi;
87
+
88
+ constructor(private readonly http: HttpClient) {
89
+ this.items = new ConversationItemsApi(http);
90
+ }
91
+
92
+ /**
93
+ * Iterate conversation summaries matching `params`. Pages are fetched
94
+ * lazily as the iterator is consumed (`limit` sets the page size,
95
+ * `next` the starting cursor); stop early to stop fetching.
96
+ */
97
+ async *list(
98
+ params?: ConversationListParams,
99
+ ): AsyncIterable<ConversationSummary> {
100
+ let next: string | undefined = params?.next;
101
+ do {
102
+ const page = await this.http.request<Paginated<ConversationSummary>>({
103
+ method: "GET",
104
+ path: "/v1/conversations",
105
+ query: { ...params, next } as Record<string, unknown>,
106
+ });
107
+ for (const c of page.records) yield c;
108
+ next = page.next ?? undefined;
109
+ } while (next);
110
+ }
111
+
112
+ /**
113
+ * Responses-API-style retrieve: load the state of a conversation as of
114
+ * one item — the full input history, output, system instructions, and
115
+ * tool definitions of that turn.
116
+ *
117
+ * When `itemId` is omitted, the latest LLM turn is used: the first
118
+ * item (in descending order) whose `node_type` is `"assistant"` or
119
+ * whose `operation_name` is `"chat"`, falling back to the first item
120
+ * with a non-null `output_message`. Returns `null` when the
121
+ * conversation has no items.
122
+ *
123
+ * For the full per-turn transcript instead, iterate
124
+ * `items.list(conversationId, { order: "asc" })`.
125
+ */
126
+ async retrieve(
127
+ conversationId: string,
128
+ itemId?: string,
129
+ ): Promise<ConversationResponse | null> {
130
+ const targetId = itemId ?? (await this.findLatestTurnId(conversationId));
131
+ if (targetId === null) return null;
132
+
133
+ const detail = await this.items.get(conversationId, targetId, {
134
+ include: RESPONSE_INCLUDES,
135
+ });
136
+ const outputMessages =
137
+ detail.gen_ai_output_messages ??
138
+ (detail.output_message ? [detail.output_message] : []);
139
+ return {
140
+ conversation_id: conversationId,
141
+ response_id: detail.response_id ?? null,
142
+ item_id: detail.id,
143
+ created_at: detail.created_at,
144
+ model:
145
+ detail.model_name ??
146
+ detail.response_model ??
147
+ detail.request_model ??
148
+ null,
149
+ provider_name: detail.provider_name ?? null,
150
+ // The single-item route returns the FULL input history here.
151
+ input_messages: normalizeMessages(detail.input_messages),
152
+ output_messages: normalizeMessages(outputMessages),
153
+ system_instructions: detail.system_instructions ?? null,
154
+ tool_definitions: detail.tool_definitions ?? null,
155
+ };
156
+ }
157
+
158
+ /** Scan items in descending order for the most recent LLM turn. */
159
+ private async findLatestTurnId(
160
+ conversationId: string,
161
+ ): Promise<string | null> {
162
+ let fallback: ConversationItem | null = null;
163
+ for await (const item of this.items.list(conversationId, {
164
+ order: "desc",
165
+ })) {
166
+ if (item.node_type === "assistant" || item.operation_name === "chat") {
167
+ return item.id;
168
+ }
169
+ if (fallback === null && item.output_message != null) fallback = item;
170
+ }
171
+ return fallback?.id ?? null;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Defensive normalization for {@link ConversationsApi.retrieve} only:
177
+ * older DP deployments emitted `tool_call_response` parts with a legacy
178
+ * `result` key instead of the semconv `response` key. Map it across so
179
+ * replayed history is always semconv-shaped.
180
+ */
181
+ function normalizeMessages<T extends { parts: MessagePart[] }>(
182
+ messages: T[],
183
+ ): T[] {
184
+ return messages.map((message) => ({
185
+ ...message,
186
+ parts: message.parts.map(normalizePart),
187
+ }));
188
+ }
189
+
190
+ function normalizePart(part: MessagePart): MessagePart {
191
+ if (part.type !== "tool_call_response") return part;
192
+ const legacy = part as ToolCallResponsePart & { result?: unknown };
193
+ if (legacy.response !== undefined || legacy.result === undefined) {
194
+ return part;
195
+ }
196
+ const { result, ...rest } = legacy;
197
+ return { ...rest, response: result };
198
+ }
@@ -21,21 +21,21 @@ export type FileUploadBody =
21
21
  export class FileVersionsApi {
22
22
  constructor(private readonly http: HttpClient) {}
23
23
 
24
- list(fileId: string, params?: ListParams): Promise<Paginated<FileResource>> {
25
- return this.http.request<Paginated<FileResource>>({
26
- method: "GET",
27
- path: `/v1/files/${encodeURIComponent(fileId)}/versions`,
28
- query: params as Record<string, unknown> | undefined,
29
- });
30
- }
31
-
32
- async *listAll(
24
+ /**
25
+ * Iterate versions of a file. Pages are fetched lazily as the
26
+ * iterator is consumed; stop early to stop fetching.
27
+ */
28
+ async *list(
33
29
  fileId: string,
34
30
  params?: ListParams,
35
31
  ): AsyncIterable<FileResource> {
36
32
  let next: string | undefined = params?.next;
37
33
  do {
38
- const page = await this.list(fileId, { ...params, next });
34
+ const page = await this.http.request<Paginated<FileResource>>({
35
+ method: "GET",
36
+ path: `/v1/files/${encodeURIComponent(fileId)}/versions`,
37
+ query: { ...params, next } as Record<string, unknown>,
38
+ });
39
39
  for (const f of page.records) yield f;
40
40
  next = page.next ?? undefined;
41
41
  } while (next);
@@ -65,18 +65,19 @@ export class FilesApi {
65
65
  this.versions = new FileVersionsApi(http);
66
66
  }
67
67
 
68
- list(params?: FileListParams): Promise<Paginated<FileResource>> {
69
- return this.http.request<Paginated<FileResource>>({
70
- method: "GET",
71
- path: "/v1/files",
72
- query: params as Record<string, unknown> | undefined,
73
- });
74
- }
75
-
76
- async *listAll(params?: FileListParams): AsyncIterable<FileResource> {
68
+ /**
69
+ * Iterate files matching `params`. Pages are fetched lazily as the
70
+ * iterator is consumed (`limit` sets the page size, `next` the
71
+ * starting cursor); stop early to stop fetching.
72
+ */
73
+ async *list(params?: FileListParams): AsyncIterable<FileResource> {
77
74
  let next: string | undefined = params?.next;
78
75
  do {
79
- const page = await this.list({ ...params, next });
76
+ const page = await this.http.request<Paginated<FileResource>>({
77
+ method: "GET",
78
+ path: "/v1/files",
79
+ query: { ...params, next } as Record<string, unknown>,
80
+ });
80
81
  for (const f of page.records) yield f;
81
82
  next = page.next ?? undefined;
82
83
  } while (next);
@@ -84,18 +84,19 @@ export class TasksApi {
84
84
  this.runs = new TaskRunsApi(http);
85
85
  }
86
86
 
87
- list(params?: TaskListParams): Promise<Paginated<Task>> {
88
- return this.http.request<Paginated<Task>>({
89
- method: "GET",
90
- path: "/v1/tasks",
91
- query: params as Record<string, unknown> | undefined,
92
- });
93
- }
94
-
95
- async *listAll(params?: TaskListParams): AsyncIterable<Task> {
87
+ /**
88
+ * Iterate tasks matching `params`. Pages are fetched lazily as the
89
+ * iterator is consumed (`limit` sets the page size, `next` the
90
+ * starting cursor); stop early to stop fetching.
91
+ */
92
+ async *list(params?: TaskListParams): AsyncIterable<Task> {
96
93
  let next: string | undefined = params?.next;
97
94
  do {
98
- const page = await this.list({ ...params, next });
95
+ const page = await this.http.request<Paginated<Task>>({
96
+ method: "GET",
97
+ path: "/v1/tasks",
98
+ query: { ...params, next } as Record<string, unknown>,
99
+ });
99
100
  for (const t of page.records) yield t;
100
101
  next = page.next ?? undefined;
101
102
  } while (next);
package/src/runner.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import { HttpClient } from "./http.js";
10
10
  import { TasksApi } from "./runner-resources/tasks.js";
11
11
  import { FilesApi } from "./runner-resources/files.js";
12
+ import { ConversationsApi } from "./runner-resources/conversations.js";
12
13
  import type { IntrospectionClient } from "./client.js";
13
14
 
14
15
  /**
@@ -23,7 +24,7 @@ export type RunnerSource =
23
24
  /**
24
25
  * Live handle to a Data Plane sandbox. Holds the bearer JWT, the DP
25
26
  * endpoint URL, and the runtime/experiment context, and exposes the
26
- * runner-bound `tasks` and `files` namespaces.
27
+ * runner-bound `tasks`, `files`, and `conversations` namespaces.
27
28
  *
28
29
  * In v1 of the agent-session-based design, token refresh is handled
29
30
  * server-side by the DP materializer attached to the agent session — the
@@ -39,6 +40,7 @@ export class Runner {
39
40
  // Public API surfaces.
40
41
  readonly tasks: TasksApi;
41
42
  readonly files: FilesApi;
43
+ readonly conversations: ConversationsApi;
42
44
 
43
45
  constructor(
44
46
  private readonly client: IntrospectionClient,
@@ -49,6 +51,7 @@ export class Runner {
49
51
  this.http = this.buildHttp();
50
52
  this.tasks = new TasksApi(this.guardedHttp(this.http));
51
53
  this.files = new FilesApi(this.guardedHttp(this.http));
54
+ this.conversations = new ConversationsApi(this.guardedHttp(this.http));
52
55
  }
53
56
 
54
57
  // --- public accessors ---
@@ -113,7 +116,8 @@ export class Runner {
113
116
 
114
117
  /**
115
118
  * Best-effort close — flips a local `isClosed` flag so subsequent
116
- * `runner.tasks` / `runner.files` calls throw a friendly error. No
119
+ * `runner.tasks` / `runner.files` / `runner.conversations` calls
120
+ * throw a friendly error. No
117
121
  * server-side revoke is performed; future work will route a revoke via
118
122
  * the CP locator-token path.
119
123
  */
package/src/utils.ts CHANGED
@@ -1,19 +1,23 @@
1
1
  import { DiagLogLevel, DiagLogger } from "@opentelemetry/api";
2
2
  import { HttpsProxyAgent } from "https-proxy-agent";
3
- import { resolveForwardProxyUrl } from "@introspection-sdk/introspection-proxy";
3
+ import { getProxyForUrl } from "proxy-from-env";
4
4
 
5
5
  /**
6
6
  * Attach a forward-proxy agent to OTLP exporter options when a proxy is
7
- * configured in the environment.
7
+ * configured for the exporter's endpoint.
8
8
  *
9
- * Note: the OTLP proto exporters use Node's `http`/`https` stack, so this uses
9
+ * The OTLP proto exporters use Node's `http`/`https` stack, so this uses
10
10
  * `HttpsProxyAgent` (an `http.Agent`) rather than the undici dispatcher from
11
- * `./proxy.js` — a dispatcher only applies to `fetch`. The proxy *URL* is
12
- * resolved by the shared {@link resolveForwardProxyUrl} so fetch and OTLP
13
- * traffic agree on which proxy to use.
11
+ * `@introspection-sdk/introspection-proxy` — a dispatcher only applies to
12
+ * `fetch`. Unlike undici's `EnvHttpProxyAgent`, `HttpsProxyAgent` ignores
13
+ * `NO_PROXY`, so we resolve the proxy per-endpoint with `proxy-from-env`'s
14
+ * {@link getProxyForUrl}, which returns `""` when the host matches `NO_PROXY`.
15
+ * This keeps in-cluster endpoints (e.g. `*.svc.cluster.local`) on a direct
16
+ * connection instead of tunnelling them through the egress proxy, which has no
17
+ * route for them.
14
18
  */
15
- export function withOtlpHttpsProxy<T extends object>(options: T): T {
16
- const proxyUrl = resolveForwardProxyUrl();
19
+ export function withOtlpHttpsProxy<T extends { url?: string }>(options: T): T {
20
+ const proxyUrl = options.url ? getProxyForUrl(options.url) : "";
17
21
  if (!proxyUrl) return options;
18
22
 
19
23
  return {