@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/experiments.d.ts +7 -3
- package/dist/resources/experiments.d.ts.map +1 -1
- package/dist/resources/experiments.js +11 -10
- package/dist/resources/experiments.js.map +1 -1
- package/dist/resources/recipes.d.ts +7 -4
- package/dist/resources/recipes.d.ts.map +1 -1
- package/dist/resources/recipes.js +11 -10
- package/dist/resources/recipes.js.map +1 -1
- package/dist/resources/runtimes.d.ts +7 -3
- package/dist/resources/runtimes.d.ts.map +1 -1
- package/dist/resources/runtimes.js +23 -19
- package/dist/resources/runtimes.js.map +1 -1
- package/dist/runner-resources/conversations.d.ts +69 -0
- package/dist/runner-resources/conversations.d.ts.map +1 -0
- package/dist/runner-resources/conversations.js +167 -0
- package/dist/runner-resources/conversations.js.map +1 -0
- package/dist/runner-resources/files.d.ts +12 -5
- package/dist/runner-resources/files.d.ts.map +1 -1
- package/dist/runner-resources/files.js +21 -18
- package/dist/runner-resources/files.js.map +1 -1
- package/dist/runner-resources/tasks.d.ts +7 -3
- package/dist/runner-resources/tasks.d.ts.map +1 -1
- package/dist/runner-resources/tasks.js +11 -9
- package/dist/runner-resources/tasks.js.map +1 -1
- package/dist/runner.d.ts +5 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +6 -2
- package/dist/runner.js.map +1 -1
- package/dist/utils.d.ts +12 -6
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +11 -7
- package/dist/utils.js.map +1 -1
- package/package.json +7 -6
- package/src/index.ts +23 -1
- package/src/proxy-from-env.d.ts +7 -0
- package/src/resources/experiments.ts +11 -11
- package/src/resources/recipes.ts +11 -11
- package/src/resources/runtimes.ts +22 -20
- package/src/runner-resources/conversations.ts +198 -0
- package/src/runner-resources/files.ts +21 -20
- package/src/runner-resources/tasks.ts +11 -10
- package/src/runner.ts +6 -2
- 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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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.
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
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 `
|
|
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`
|
|
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 {
|
|
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
|
|
7
|
+
* configured for the exporter's endpoint.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
16
|
-
const proxyUrl =
|
|
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 {
|