Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
487ad53
feat(sdk/typescript): add core TypeScript SDK package
Adarsh9977 Jun 11, 2026
de7d91e
chore(proto): move event.proto into sdk/python/adrian/proto
Adarsh9977 Jun 11, 2026
5c047af
fix(ts-core): keep proto layout aligned with main
Adarsh9977 Jun 11, 2026
7271a49
fix(sdk/typescript): address core team feedback
Adarsh9977 Jun 11, 2026
3d3b80c
removed unnecessary readme
Adarsh9977 Jun 11, 2026
d889006
fix
Adarsh9977 Jun 11, 2026
480382d
feat(sdk/typescript): add OpenAI provider package
Adarsh9977 Jun 11, 2026
064bde3
removed core only documentation
Adarsh9977 Jun 11, 2026
8f44bca
Fix 4003 reconnect backoff and refresh TypeScript SDK docs
Adarsh9977 Jun 11, 2026
c20e8cf
updated package.json
Adarsh9977 Jun 11, 2026
49a711d
integrity hash
Adarsh9977 Jun 15, 2026
61bde28
fail open on missing toolcall id
Adarsh9977 Jun 15, 2026
910be4b
updated 4003 reconnect
Adarsh9977 Jun 15, 2026
31d2eba
reconnect immediately on normal close like python sdk
Adarsh9977 Jun 15, 2026
0afe980
patch applied
Adarsh9977 Jun 16, 2026
25c4a2e
updated patterns and strategies
Adarsh9977 Jun 16, 2026
921251f
added patterns test
Adarsh9977 Jun 16, 2026
6db9f68
reabse ts-core
Adarsh9977 Jun 17, 2026
b77caf6
Merge branch 'ts-core' into ts-openai
Adarsh9977 Jun 17, 2026
77295df
Merge branch 'secureagentics:main' into ts-openai
Adarsh9977 Jun 18, 2026
0ca3ff6
restore openai stream interface compatibility
Adarsh9977 Jun 18, 2026
608f920
code refactor and multiple fixes
Adarsh9977 Jun 18, 2026
15a3ae0
remove random uuid generation
Adarsh9977 Jun 23, 2026
ec78515
Merge branch 'main' into ts-openai
Adarsh9977 Jun 24, 2026
2da1952
preserve tool-call gating
Adarsh9977 Jun 24, 2026
5002463
stop gating llm_end tool-call metadata
Adarsh9977 Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 113 additions & 39 deletions sdk/typescript/packages/core/src/capture/common.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { randomUUID } from "node:crypto";
import { currentConfig } from "../config.js";
import type { AdrianCallbackHandler } from "../handler.js";
import { runWithInvocationId } from "../context.js";
import { assertToolCallsAllowed } from "../policy.js";
import { getWebSocketClient } from "../registry.js";
import { getInvocationId, runWithInvocationId } from "../context.js";
import type { CallbackMetadata, ChatMessage, LlmEndData, TokenUsage, ToolArgs, ToolCallRecord } from "../types.js";

/** Gate tool calls after the paired LLM event has been emitted (maps tool-call ids on the WS client). */
export async function gateLlmEndData(end: LlmEndData): Promise<void> {
await assertToolCallsAllowed(
end.toolCalls.map((call) => call.id),
getWebSocketClient(),
currentConfig()?.blockTimeout ?? 30,
);
/** LLM end tool-call metadata is informational and never blockable. */
export async function gateLlmEndData(_end: LlmEndData): Promise<void> {
}

export interface LlmCaptureInput {
Expand All @@ -33,7 +25,8 @@ export async function captureLlmCall<T>(
if (!handler) return execute();

const runId = randomUUID();
return runWithInvocationId(randomUUID(), async () => {
const invocationId = getInvocationId();
const run = async () => {
await handler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata });
try {
const result = await execute();
Expand All @@ -45,7 +38,31 @@ export async function captureLlmCall<T>(
await handler.handleLLMError(error, runId);
throw error;
}
});
};
return invocationId === null ? run() : runWithInvocationId(invocationId, run);
}

/** Wrap an LLM call that may fail before returning (e.g. streaming create). Records start+error, then re-throws. */
export async function captureLlmExecute<T>(
getHandler: () => AdrianCallbackHandler | null,
input: LlmCaptureInput,
execute: () => Promise<T>,
): Promise<T> {
try {
return await execute();
} catch (error) {
const handler = getHandler();
if (handler) {
const runId = randomUUID();
const invocationId = getInvocationId();
const run = async () => {
await handler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata });
await handler.handleLLMError(error, runId);
};
await (invocationId === null ? run() : runWithInvocationId(invocationId, run));
}
throw error;
}
}

export function captureLlmAsyncIterable<T>(
Expand All @@ -60,37 +77,94 @@ export function captureLlmAsyncIterable<T>(
if (!handler) return iterable;

const runId = randomUUID();
const invocationId = randomUUID();

async function* wrapped(): AsyncGenerator<T> {
await handler?.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata });
yield* runWithInvocationId(invocationId, async function* () {
let emitted = false;
let failed = false;
try {
for await (const chunk of iterable) {
aggregate(chunk);
yield chunk;
}
emitted = true;
const endData = await extractOutput();
await handler?.handleLLMEnd(endData, runId);
await afterPairedEmit?.(endData);
} catch (error) {
failed = true;
await handler?.handleLLMError(error, runId);
throw error;
} finally {
if (!emitted && !failed) {
const invocationId = getInvocationId();
const activeHandler = handler;

const createIterator = (): AsyncIterator<T> => {
async function* gen(): AsyncGenerator<T> {
await activeHandler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata });

const streamBody = async function* (): AsyncGenerator<T> {
let emitted = false;
let failed = false;
try {
for await (const chunk of iterable) {
aggregate(chunk);
yield chunk;
}
emitted = true;
const endData = await extractOutput();
await handler?.handleLLMEnd(endData, runId);
await activeHandler.handleLLMEnd(endData, runId);
await afterPairedEmit?.(endData);
} catch (error) {
failed = true;
await activeHandler.handleLLMError(error, runId);
throw error;
} finally {
if (!emitted && !failed) {
const endData = await extractOutput();
await activeHandler.handleLLMEnd(endData, runId);
await afterPairedEmit?.(endData);
}
}
};

if (invocationId === null) {
yield* streamBody();
} else {
yield* runWithInvocationId(invocationId, streamBody);
}
});
}
}
return gen();
};

return preserveStreamSurface(iterable, createIterator);
}

/** Keep provider stream helpers (tee, toReadableStream, controller) while intercepting iteration. */
function preserveStreamSurface<T>(
source: AsyncIterable<T>,
createIterator: () => AsyncIterator<T>,
): AsyncIterable<T> {
const iterable: AsyncIterable<T> = { [Symbol.asyncIterator]: createIterator };
if (!source || typeof source !== "object") return iterable;

const stream = source as Record<PropertyKey, unknown> & AsyncIterable<T>;
return new Proxy(iterable, {
get(_target, prop, receiver) {
if (prop === Symbol.asyncIterator) return createIterator;
if (prop === "tee") {
return () => teeCapturingStream(createIterator).map((branch) => preserveStreamSurface(stream, branch));
}
const value = Reflect.get(stream, prop, stream);
if (typeof value === "function") return value.bind(receiver);
return value;
},
});
}

/** Split one capturing iterator into two branches without restarting capture. */
function teeCapturingStream<T>(
createIterator: () => AsyncIterator<T>,
): [() => AsyncIterator<T>, () => AsyncIterator<T>] {
const left: Array<Promise<IteratorResult<T>>> = [];
const right: Array<Promise<IteratorResult<T>>> = [];
const iterator = createIterator();

const branchIterator = (queue: Array<Promise<IteratorResult<T>>>) => (): AsyncIterator<T> => ({
next: () => {
if (queue.length === 0) {
const result = iterator.next();
left.push(result);
right.push(result);
}
return queue.shift()!;
},
return: (value) => iterator.return?.(value) ?? Promise.resolve({ done: true as const, value: undefined }),
throw: (error) => iterator.throw?.(error) ?? Promise.reject(error),
});

return wrapped();
return [branchIterator(left), branchIterator(right)];
}

export function normalizeMessages(input: unknown): ChatMessage[] {
Expand Down
5 changes: 5 additions & 0 deletions sdk/typescript/packages/openai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @secureagentics/adrian-openai

OpenAI SDK instrumentation for Adrian security monitoring.

Full documentation: [sdk/typescript/README.md](../../README.md)
52 changes: 52 additions & 0 deletions sdk/typescript/packages/openai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@secureagentics/adrian-openai",
"version": "1.0.0",
"description": "OpenAI SDK instrumentation for Adrian security monitoring.",
"license": "Apache-2.0",
"author": "Secure Agentics <support@secureagentics.ai>",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"keywords": [
"openai",
"ai",
"agents",
"security",
"monitoring"
],
"dependencies": {
"@secureagentics/adrian": "^1.0.0"
},
"peerDependencies": {
"openai": ">=4.0.0"
},
"peerDependenciesMeta": {
"openai": {
"optional": true
}
},
"devDependencies": {
"@secureagentics/adrian": "file:../core",
"@types/node": "^25.9.1",
"tsup": "^8.5.1",
"typescript": "^6.0.3",
"vitest": "^4.1.7"
}
}
Loading